diff --git a/python/intellij.python.community.impl.iml b/python/intellij.python.community.impl.iml
index 58aab2a53ac6..f28d30771660 100644
--- a/python/intellij.python.community.impl.iml
+++ b/python/intellij.python.community.impl.iml
@@ -110,6 +110,7 @@
+
diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties
index 1f876a4bc90c..f597e8de4697 100644
--- a/python/pluginResources/messages/PyBundle.properties
+++ b/python/pluginResources/messages/PyBundle.properties
@@ -1217,6 +1217,24 @@ python.packaging.repository.form.password=Password:
python.packaging.repository.form.default.name=Package Repository
python.packaging.loading.packages.progress.text=Loading packages\u2026
notification.group.packaging=Python packaging
+notification.group.packaging.toolwindow=Python Packages
+python.packaging.install.progress=Installing package {0}
+python.packaging.uninstall.progress=Uninstalling package {0}
+python.packaging.list.progress=Reading installed python packages
+python.packaging.could.not.parse.response=
Could not parse the response for the package {0} from repository {1}
+python.packaging.notification.installed=Package {0} installed
+python.packaging.notification.deleted=Package {0} deleted
+python.packaging.button.install.package=Install package
+python.packaging.no.package.info=Could not read package information
+python.packages.request.failed=Request failed.
+
+# Conda
+conda.packaging.install.progress=Installing conda package {0}
+conda.packaging.uninstall.progress=Uninstalling conda package {0}
+conda.packaging.list.progress=Reading installed conda packages
+conda.packaging.cache.update.progress=Updating available conda packages list
+conda.packaging.button.install.with.conda=Install with conda
+conda.packaging.button.install.with.pip=Install with pip
# Python Packages toolwindow
python.toolwindow.packages.installed.label=Installed
@@ -1225,16 +1243,11 @@ python.toolwindow.packages.custom.repo.invalid={0} (Authorization failed)
python.toolwindow.packages.documentation.link=Documentation
python.toolwindow.packages.no.interpreter.text=Select an interpreter to see the installed packages
python.toolwindow.packages.latest.version.label=latest
-python.toolwindow.packages.install.button=Install
python.toolwindow.packages.delete.package=Delete Package
python.toolwindow.packages.search.text.placeholder=Search for more packages
python.toolwindow.packages.description.panel.placeholder=Select a package to view documentation
-python.toolwindow.packages.request.failed=Request failed.
python.toolwindow.packages.no.description.placeholder=Package author did not provide a description.
-python.toolwindow.packages.remote.interpreter.placeholder=Local interpreter is required to view package documentation.
-python.toolwindow.packages.collecting.packages.task.title=Collecting packages
python.toolwindow.packages.rst.parsing.failed=Failed to parse the description
-python.toolwindow.packages.no.documentation=No documentation found in current repository
python.toolwindow.packages.manage.repositories.action=Manage repositories
python.toolwindow.packages.reload.repositories.action=Reload all repositories
python.toolwindow.packages.add.package.action=Add Package
diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml
index b2042d1bb2a8..3a5412f6ffa8 100644
--- a/python/src/META-INF/python-core-common.xml
+++ b/python/src/META-INF/python-core-common.xml
@@ -483,6 +483,8 @@
+
@@ -498,6 +500,8 @@
+
+
@@ -590,6 +594,10 @@
+
+
@@ -685,6 +693,9 @@
+
+
+
diff --git a/python/src/com/jetbrains/extensions/PythonProjectExt.kt b/python/src/com/jetbrains/extensions/PythonProjectExt.kt
new file mode 100644
index 000000000000..e36fc75df919
--- /dev/null
+++ b/python/src/com/jetbrains/extensions/PythonProjectExt.kt
@@ -0,0 +1,14 @@
+// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+@file:JvmName("PythonProjectExt")
+
+package com.jetbrains.extensions
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.modules
+import com.jetbrains.python.sdk.PythonSdkType
+import com.jetbrains.python.sdk.PythonSdkUtil
+
+val Project.hasPython: Boolean
+ get() = modules.asSequence()
+ .map { PythonSdkUtil.findPythonSdk(it) }
+ .any { it != null && it.sdkType is PythonSdkType }
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/PyPIPackageRanking.kt b/python/src/com/jetbrains/python/packaging/PyPIPackageRanking.kt
index 9d307cca6892..c3b4f57bd8ab 100644
--- a/python/src/com/jetbrains/python/packaging/PyPIPackageRanking.kt
+++ b/python/src/com/jetbrains/python/packaging/PyPIPackageRanking.kt
@@ -3,7 +3,8 @@ package com.jetbrains.python.packaging
import com.google.common.io.Resources
import com.google.gson.Gson
-import com.intellij.openapi.application.ApplicationManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import java.util.*
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
@@ -22,16 +23,19 @@ object PyPIPackageRanking {
val names: Sequence
get() = myPackageRank.asSequence().map { it.key }
- fun reload() {
- ApplicationManager.getApplication().assertIsNonDispatchThread();
- val gson = Gson()
- val resource = PyPIPackageRanking::class.java.getResource("/packaging/pypi-ranking.json") ?: error("Python package ranking not found")
- val array = Resources.asCharSource(resource, Charsets.UTF_8).openBufferedStream().use {
- gson.fromJson(it, Array>::class.java)
+ suspend fun reload() {
+ withContext(Dispatchers.IO) {
+ val gson = Gson()
+ val resource = PyPIPackageRanking::class.java.getResource("/packaging/pypi-ranking.json") ?: error("Python package ranking not found")
+ val array = Resources.asCharSource(resource, Charsets.UTF_8).openBufferedStream().use {
+ gson.fromJson(it, Array>::class.java)
+ }
+ val newRanked = array.asSequence()
+ .map { Pair(it[0].lowercase(), it[1].toInt()) }
+ .toMap(LinkedHashMap())
+ withContext(Dispatchers.Main) {
+ myPackageRank = Collections.unmodifiableMap(newRanked)
+ }
}
- val newRanked = array.asSequence()
- .map { Pair(it[0].toLowerCase(), it[1].toInt()) }
- .toMap(LinkedHashMap())
- myPackageRank = Collections.unmodifiableMap(newRanked)
}
}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/PyPackagesUpdater.java b/python/src/com/jetbrains/python/packaging/PyPackagesUpdater.java
index 9b75c34999b6..e9aefcec6beb 100644
--- a/python/src/com/jetbrains/python/packaging/PyPackagesUpdater.java
+++ b/python/src/com/jetbrains/python/packaging/PyPackagesUpdater.java
@@ -44,12 +44,6 @@ public class PyPackagesUpdater implements StartupActivity.Background {
@Override
public void runActivity(@NotNull Project project) {
if (ApplicationManager.getApplication().isUnitTestMode()) return;
- try {
- PyPIPackageRanking.INSTANCE.reload();
- }
- catch (Exception e) {
- LOG.warn(e);
- }
if (!checkNeeded(project)) return;
diff --git a/python/src/com/jetbrains/python/packaging/cache/PythonPackageCache.kt b/python/src/com/jetbrains/python/packaging/cache/PythonPackageCache.kt
new file mode 100644
index 000000000000..6a855a684ca7
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/cache/PythonPackageCache.kt
@@ -0,0 +1,11 @@
+// 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.cache
+
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+interface PythonPackageCache {
+ val packages: List
+ operator fun contains(key: K): Boolean
+ fun isEmpty(): Boolean
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/cache/PythonSimpleRepositoryCache.kt b/python/src/com/jetbrains/python/packaging/cache/PythonSimpleRepositoryCache.kt
new file mode 100644
index 000000000000..fc3024c90c68
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/cache/PythonSimpleRepositoryCache.kt
@@ -0,0 +1,91 @@
+// 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.cache
+
+import com.intellij.openapi.application.ApplicationInfo
+import com.intellij.openapi.application.ApplicationNamesInfo
+import com.intellij.openapi.components.service
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.util.io.HttpRequests
+import com.jetbrains.python.packaging.repository.PyPackageRepositories
+import com.jetbrains.python.packaging.repository.PyPackageRepository
+import com.jetbrains.python.packaging.repository.withBasicAuthorization
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+import javax.swing.text.MutableAttributeSet
+import javax.swing.text.html.HTML
+import javax.swing.text.html.HTMLEditorKit
+import javax.swing.text.html.parser.ParserDelegator
+
+@ApiStatus.Experimental
+object PythonSimpleRepositoryCache : PythonPackageCache {
+
+ private var cache: Map> = emptyMap()
+
+ val repositories: List
+ get() = cache.keys.toList()
+ override val packages: List
+ get() = cache.values.asSequence().flatten().toList()
+
+ private val userAgent: String
+ get() = "${ApplicationNamesInfo.getInstance().productName}/${ApplicationInfo.getInstance().fullVersion}"
+
+ suspend fun refresh() {
+ val service = service()
+ withContext(Dispatchers.IO) {
+ val newCache = mutableMapOf>()
+ service.repositories.forEach {
+ try {
+ newCache[it] = loadFrom(it)
+ }
+ catch (ex: Exception) {
+ thisLogger().error("could not refresh repository ${it.repositoryUrl}")
+ service.markInvalid(it.repositoryUrl!!)
+ }
+ }
+ withContext(Dispatchers.Main) {
+ cache = newCache
+ }
+ }
+ }
+
+ @Suppress("BlockingMethodInNonBlockingContext")
+ private suspend fun loadFrom(repository: PyPackageRepository): List {
+ return withContext(Dispatchers.IO) {
+ val packages = mutableListOf()
+ HttpRequests.request(repository.repositoryUrl!!)
+ .userAgent(userAgent)
+ .withBasicAuthorization(repository)
+ .connect { request ->
+ ParserDelegator().parse(request.reader, object : HTMLEditorKit.ParserCallback() {
+ var myTag: HTML.Tag? = null
+ override fun handleStartTag(tag: HTML.Tag, set: MutableAttributeSet, i: Int) {
+ myTag = tag
+ }
+
+ override fun handleText(data: CharArray, pos: Int) {
+ if ("a" == myTag?.toString()) {
+ var packageName = String(data)
+ if (packageName.endsWith("/")) {
+ packageName = packageName.substring(0, packageName.indexOf("/"))
+ }
+ packages.add(packageName)
+ }
+ }
+
+ override fun handleEndTag(t: HTML.Tag, pos: Int) {
+ myTag = null
+ }
+ }, true)
+
+ }
+ packages
+ }
+ }
+
+ operator fun get(key: PyPackageRepository): List? = cache[key]
+
+ override fun isEmpty(): Boolean = cache.isEmpty()
+
+ override fun contains(key: PyPackageRepository): Boolean = key in cache
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/common/packages.kt b/python/src/com/jetbrains/python/packaging/common/packages.kt
new file mode 100644
index 000000000000..7ba2107ffc33
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/common/packages.kt
@@ -0,0 +1,81 @@
+// 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.repository.PyEmptyPackagePackageRepository
+import com.jetbrains.python.packaging.repository.PyPackageRepository
+import org.jetbrains.annotations.Nls
+
+open class PythonPackage(val name: String, val version: String)
+
+interface PythonPackageDetails {
+
+ val name: String
+ val availableVersions: List
+ val repository: PyPackageRepository
+ val summary: String?
+ val description: String?
+ val descriptionContentType: String?
+ val documentationUrl: String?
+ fun toPackageSpecification(version: String? = null): PythonPackageSpecification
+}
+
+data class PythonSimplePackageDetails(
+ override val name: String,
+ override val availableVersions: List = emptyList(),
+ override val repository: PyPackageRepository,
+ override val summary: String? = null,
+ @Nls override val description: String? = null,
+ override val descriptionContentType: String? = null,
+ override val documentationUrl: String? = null) : PythonPackageDetails {
+
+ override fun toPackageSpecification(version: String?): PythonSimplePackageSpecification {
+ return PythonSimplePackageSpecification(name, version, repository)
+ }
+}
+
+class EmptyPythonPackageDetails(override val name: String, @Nls override val description: String? = null) : PythonPackageDetails {
+ override val availableVersions: List = emptyList()
+ override val repository: PyPackageRepository = PyEmptyPackagePackageRepository
+ override val summary: String? = null
+ override val descriptionContentType: String? = null
+ override val documentationUrl: String? = null
+ override fun toPackageSpecification(version: String?) = error("Using EmptyPythonPackageDetails for specification")
+}
+
+interface PythonPackageSpecification {
+ // todo[akniazev]: add version specs and use them in buildInstallationString
+ val name: String
+ val version: String?
+ val repository: PyPackageRepository?
+
+ fun buildInstallationString(): List {
+ return listOf("$name${if (version != null) "==$version" else ""}")
+ }
+}
+
+interface PythonLocationBasedPackageSpecification : PythonPackageSpecification {
+ val location: String
+ val editable: Boolean
+ val prefix: String
+ override val version: String?
+ get() = null
+ override val repository: PyPackageRepository?
+ get() = null
+ override fun buildInstallationString(): List = if (editable) listOf("-e", "$prefix$location") else listOf("$prefix$location")
+}
+
+data class PythonSimplePackageSpecification(override val name: String,
+ override val version: String?,
+ override val repository: PyPackageRepository?) : PythonPackageSpecification
+
+data class PythonLocalPackageSpecification(override val name: String,
+ override val location: String,
+ override val editable: Boolean) : PythonLocationBasedPackageSpecification {
+ override val prefix: String = "file://"
+}
+
+data class PythonVcsPackageSpecification(override val name: String,
+ override val location: String,
+ override val prefix: String,
+ override val editable: Boolean) : PythonLocationBasedPackageSpecification
+
diff --git a/python/src/com/jetbrains/python/packaging/common/util.kt b/python/src/com/jetbrains/python/packaging/common/util.kt
new file mode 100644
index 000000000000..7da3f9a2e10d
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/common/util.kt
@@ -0,0 +1,44 @@
+// 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.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.PyPIPackageRanking
+import com.jetbrains.python.packaging.conda.CondaPackageManager
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.packaging.pip.PipPythonPackageManager
+import com.jetbrains.python.sdk.PythonSdkUtil
+import org.jetbrains.annotations.ApiStatus
+
+object PackageManagerHolder {
+ private val cache = mutableMapOf()
+
+ fun forSdk(project: Project, sdk: Sdk): PythonPackageManager? {
+ if (sdk.homePath in cache) return cache[sdk.homePath] // todo[akniazev] replace with sdk key
+
+ val manager = when {
+ PythonSdkUtil.isConda(sdk) -> CondaPackageManager(project, sdk) // todo[akniazev] extract to an extension point
+ else -> PipPythonPackageManager(project, sdk)
+ }
+ cache[sdk.homePath!!] = manager
+
+ return manager
+ }
+}
+
+@ApiStatus.Experimental
+interface PythonPackageManagementListener {
+ fun packagesChanged(sdk: Sdk)
+}
+
+internal val RANKING_AWARE_PACKAGE_NAME_COMPARATOR: java.util.Comparator = Comparator { name1, name2 ->
+ val ranking = PyPIPackageRanking.packageRank
+ val rank1 = ranking[name1.lowercase()]
+ val rank2 = ranking[name2.lowercase()]
+ return@Comparator when {
+ rank1 != null && rank2 == null -> -1
+ rank1 == null && rank2 != null -> 1
+ rank1 != null && rank2 != null -> rank2 - rank1
+ else -> String.CASE_INSENSITIVE_ORDER.compare(name1, name2)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageCache.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageCache.kt
new file mode 100644
index 000000000000..809c5ff26a13
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageCache.kt
@@ -0,0 +1,90 @@
+// 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.conda
+
+import com.intellij.execution.process.CapturingProcessHandler
+import com.intellij.execution.target.TargetProgressIndicator
+import com.intellij.execution.target.TargetedCommandLineBuilder
+import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.progress.withBackgroundProgressIndicator
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.util.text.StringUtil
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.cache.PythonPackageCache
+import com.jetbrains.python.packaging.common.RANKING_AWARE_PACKAGE_NAME_COMPARATOR
+import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
+import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
+import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
+import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
+import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
+import com.jetbrains.python.sdk.getOrCreateAdditionalData
+import com.jetbrains.python.sdk.targetEnvConfiguration
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+object CondaPackageCache : PythonPackageCache {
+ private var cache: Map> = emptyMap()
+
+ override val packages: List
+ get() = cache.keys.toList()
+
+ suspend fun refreshAll(sdk: Sdk, project: Project) {
+ withContext(Dispatchers.IO) {
+ val pathOnTarget = (sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env.fullCondaPathOnTarget
+ val targetConfig = sdk.targetEnvConfiguration
+
+ val command = PyCondaCommand(pathOnTarget, targetConfig, project)
+ val baseConda = PyCondaEnv.getEnvs(command).getOrThrow()
+ .first { it.envIdentity is PyCondaEnvIdentity.UnnamedEnv && it.envIdentity.isBase }
+
+ val helpersAware = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project)
+ val helpers = helpersAware.preparePyCharmHelpers()
+
+
+ val targetReq = targetConfig?.createEnvironmentRequest(project) ?: LocalTargetEnvironmentRequest()
+ val commandLineBuilder = TargetedCommandLineBuilder(targetReq)
+ val targetEnv = targetReq.prepareEnvironment(TargetProgressIndicator.EMPTY)
+
+
+ val helpersPath = helpers.apply(targetEnv)
+
+ baseConda.addCondaToTargetBuilder(commandLineBuilder)
+
+ commandLineBuilder.addParameter("python")
+ commandLineBuilder.addParameter("$helpersPath/conda_packaging_tool.py")
+ commandLineBuilder.addParameter("listall")
+
+ val targetedCommandLine = commandLineBuilder.build()
+
+ val process = targetEnv.createProcess(targetedCommandLine)
+
+
+ val commandLine = targetedCommandLine.collectCommandsSynchronously()
+ val commandLineString = StringUtil.join(commandLine, " ")
+ val handler = CapturingProcessHandler(process, targetedCommandLine.charset, commandLineString)
+ thisLogger().debug("Running conda packaging tool to read available conda packages")
+ val result = withBackgroundProgressIndicator(project, PyBundle.message("conda.packaging.cache.update.progress"), cancellable = true) {
+ handler.runProcess(10 * 60 * 1000)
+ }
+ result.checkSuccess(thisLogger())
+
+ val packages = result.stdout.lineSequence()
+ .map { it.split("\t") }
+ .filterNot { it.size < 2 }
+ .filterNot { it[0].startsWith("r-") } // todo[akniazev]: make sure it's the best way to get rid of R packages
+ .groupBy({ it[0] }, { it[1] })
+ .toSortedMap(RANKING_AWARE_PACKAGE_NAME_COMPARATOR)
+
+ withContext(Dispatchers.Main) {
+ cache = packages
+ }
+ }
+ }
+
+ override fun isEmpty(): Boolean = cache.isEmpty()
+ operator fun get(name: String): List? = cache[name]
+ override fun contains(key: String): Boolean = key in cache
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt
new file mode 100644
index 000000000000..5a3282770585
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt
@@ -0,0 +1,105 @@
+// 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.conda
+
+import com.intellij.execution.process.CapturingProcessHandler
+import com.intellij.execution.target.TargetProgressIndicator
+import com.intellij.execution.target.TargetedCommandLineBuilder
+import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.progress.withBackgroundProgressIndicator
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.util.text.StringUtil
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.pip.PipBasedPackageManager
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
+import com.jetbrains.python.sdk.getOrCreateAdditionalData
+import com.jetbrains.python.sdk.targetEnvConfiguration
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.annotations.Nls
+
+@ApiStatus.Experimental
+class CondaPackageManager(project: Project, sdk: Sdk) : PipBasedPackageManager(project, sdk) {
+
+ override var installedPackages: List = emptyList()
+ private set
+
+ override val repositoryManager: CondaRepositoryManger = CondaRepositoryManger(project, sdk)
+
+ override suspend fun installPackage(specification: PythonPackageSpecification) {
+ if (specification is CondaPackageSpecification) withContext(Dispatchers.IO) {
+ runConda("install", specification.buildInstallationString() + "-y", PyBundle.message("conda.packaging.install.progress", specification.name))
+ reloadPackages()
+ }
+ else super.installPackage(specification)
+
+ }
+
+ override suspend fun uninstallPackage(pkg: PythonPackage) {
+ if (pkg is CondaPackage && !pkg.installedWithPip) withContext(Dispatchers.IO) {
+ runConda("uninstall", listOf(pkg.name, "-y"), PyBundle.message("conda.packaging.uninstall.progress", pkg.name))
+ reloadPackages()
+ }
+ else super.uninstallPackage(pkg)
+ }
+
+ override suspend fun reloadPackages() {
+ withContext(Dispatchers.IO) {
+ val result = runConda("list", emptyList(), PyBundle.message("conda.packaging.list.progress"))
+ val packages = parseCondaPackageList(result)
+
+ withContext(Dispatchers.Main) {
+ installedPackages = packages
+ }
+
+ ApplicationManager.getApplication()
+ .messageBus
+ .syncPublisher(PACKAGE_MANAGEMENT_TOPIC)
+ .packagesChanged(sdk)
+ }
+ }
+
+ private fun parseCondaPackageList(text: String): List {
+ return text.lineSequence()
+ .filterNot { it.startsWith("#") }
+ .map { line -> line.split("\\s+".toRegex()) }
+ .filterNot { it.size < 2 }
+ .map { CondaPackage(it[0], it[1], installedWithPip = (it.size >= 4 && it[3] == "pypi")) }
+ .toList()
+ }
+
+
+ private suspend fun runConda(operation: String, arguments: List, @Nls text: String): String {
+ return withContext(Dispatchers.IO) {
+ val targetConfig = sdk.targetEnvConfiguration
+ val targetReq = targetConfig?.createEnvironmentRequest(project) ?: LocalTargetEnvironmentRequest()
+ val commandLineBuilder = TargetedCommandLineBuilder(targetReq)
+ val targetEnv = targetReq.prepareEnvironment(TargetProgressIndicator.EMPTY)
+ val env = (sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env
+
+ commandLineBuilder.setExePath(env.fullCondaPathOnTarget)
+ commandLineBuilder.addParameter(operation)
+ env.addCondaEnvironmentToTargetBuilder(commandLineBuilder)
+ arguments.forEach(commandLineBuilder::addParameter)
+
+ val targetedCommandLine = commandLineBuilder.build()
+ val process = targetEnv.createProcess(targetedCommandLine)
+ val commandLine = targetedCommandLine.collectCommandsSynchronously()
+ val commandLineString = StringUtil.join(commandLine, " ")
+ val handler = CapturingProcessHandler(process, targetedCommandLine.charset, commandLineString)
+
+ val result = withBackgroundProgressIndicator(project, text, cancellable = true) {
+ handler.runProcess(10 * 60 * 1000)
+ }
+
+ result.checkSuccess(thisLogger())
+ result.stdout
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt
new file mode 100644
index 000000000000..4649f05352b1
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt
@@ -0,0 +1,22 @@
+// 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.conda
+
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.packaging.toolwindow.PythonPackagingToolwindowActionProvider
+import com.jetbrains.python.packaging.toolwindow.PythonPackageInstallAction
+import com.jetbrains.python.packaging.toolwindow.SimplePythonPackageInstallAction
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+class CondaPackagingToolwindowActionProvider : PythonPackagingToolwindowActionProvider {
+ override fun getInstallActions(details: PythonPackageDetails, packageManager: PythonPackageManager): List? {
+ if (packageManager is CondaPackageManager) {
+ return if (details is CondaPackageDetails) {
+ listOf(SimplePythonPackageInstallAction(PyBundle.message("conda.packaging.button.install.with.conda"), packageManager.project))
+ } else listOf(SimplePythonPackageInstallAction(PyBundle.message("conda.packaging.button.install.with.pip"), packageManager.project))
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaRepositoryManger.kt b/python/src/com/jetbrains/python/packaging/conda/CondaRepositoryManger.kt
new file mode 100644
index 000000000000..7f35c5068b37
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaRepositoryManger.kt
@@ -0,0 +1,51 @@
+// 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.conda
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import com.jetbrains.python.packaging.pip.PipBasedRepositoryManager
+import com.jetbrains.python.packaging.repository.PyPackageRepository
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+class CondaRepositoryManger(project: Project, sdk: Sdk) : PipBasedRepositoryManager(project, sdk) {
+
+ override val repositories: List
+ get() = listOf(CondaPackageRepository) + super.repositories
+
+ override fun allPackages(): List = CondaPackageCache.packages
+
+ override fun packagesFromRepository(repository: PyPackageRepository): List {
+ return if (repository is CondaPackageRepository) CondaPackageCache.packages else super.packagesFromRepository(repository)
+ }
+
+ override fun buildPackageDetails(rawInfo: String, spec: PythonPackageSpecification): PythonPackageDetails {
+ if (spec is CondaPackageSpecification) {
+ val detailsFromPyPI = super.buildPackageDetails(rawInfo, spec)
+ val versions = CondaPackageCache[spec.name] ?: error("No conda package versions in cache")
+
+ return CondaPackageDetails(detailsFromPyPI.name,
+ versions,
+ detailsFromPyPI.summary,
+ detailsFromPyPI.description,
+ detailsFromPyPI.descriptionContentType,
+ detailsFromPyPI.documentationUrl)
+
+ }
+ return super.buildPackageDetails(rawInfo, spec)
+ }
+
+ override suspend fun initCaches() {
+ super.initCaches()
+ if (CondaPackageCache.isEmpty()) {
+ CondaPackageCache.refreshAll(sdk, project)
+ }
+ }
+
+ override suspend fun refreshCashes() {
+ super.refreshCashes()
+ CondaPackageCache.refreshAll(sdk, project)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/conda/common.kt b/python/src/com/jetbrains/python/packaging/conda/common.kt
new file mode 100644
index 000000000000..640396f13de8
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/conda/common.kt
@@ -0,0 +1,37 @@
+// 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.conda
+
+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.repository.PyPackageRepository
+
+class CondaPackage(name: String, version: String, val installedWithPip: Boolean = false) : PythonPackage(name, version)
+
+
+class CondaPackageSpecification(override val name: String,
+ override val version: String?) : PythonPackageSpecification {
+ override val repository: PyPackageRepository = CondaPackageRepository
+
+ override fun buildInstallationString(): List {
+ return listOf("$name${if (version != null) "=$version" else ""}")
+ }
+}
+
+class CondaPackageDetails(override val name: String,
+ override val availableVersions: List = emptyList(),
+ override val summary: String? = null,
+ override val description: String? = null,
+ override val descriptionContentType: String? = null,
+ override val documentationUrl: String? = null) : PythonPackageDetails {
+ override val repository: PyPackageRepository = CondaPackageRepository
+ override fun toPackageSpecification(version: String?): PythonPackageSpecification {
+ return CondaPackageSpecification(name, version)
+ }
+}
+
+object CondaPackageRepository : PyPackageRepository("Conda", "", "") {
+ override fun createPackageSpecification(packageName: String, version: String?): PythonPackageSpecification {
+ return CondaPackageSpecification(packageName, null)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt
new file mode 100644
index 000000000000..4720f3a4088e
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt
@@ -0,0 +1,32 @@
+// 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.management
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.util.messages.Topic
+import com.jetbrains.python.packaging.common.PackageManagerHolder
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.packaging.common.PythonPackageManagementListener
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
+ abstract val installedPackages: List
+
+ abstract val repositoryManager: PythonRepositoryManager
+
+ abstract suspend fun installPackage(specification: PythonPackageSpecification)
+ abstract suspend fun uninstallPackage(pkg: PythonPackage)
+
+ abstract suspend fun reloadPackages()
+
+ companion object {
+ fun forSdk(project: Project, sdk: Sdk): PythonPackageManager? {
+ return PackageManagerHolder.forSdk(project, sdk)
+ }
+
+ @Topic.AppLevel
+ val PACKAGE_MANAGEMENT_TOPIC = Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt
new file mode 100644
index 000000000000..d81a8a2957e7
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt
@@ -0,0 +1,109 @@
+// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+@file:JvmName("PythonPackageManagerExt")
+package com.jetbrains.python.packaging.management
+
+import com.intellij.execution.RunCanceledByUserException
+import com.intellij.execution.process.CapturingProcessHandler
+import com.intellij.execution.target.TargetProgressIndicator
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.progress.EmptyProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.withBackgroundProgressIndicator
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.util.net.HttpConfigurable
+import com.jetbrains.python.PySdkBundle
+import com.jetbrains.python.PythonHelper
+import com.jetbrains.python.packaging.PyExecutionException
+import com.jetbrains.python.packaging.repository.PyPackageRepository
+import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
+import com.jetbrains.python.run.buildTargetedCommandLine
+import com.jetbrains.python.run.prepareHelperScriptExecution
+import kotlinx.coroutines.launch
+import org.jetbrains.annotations.Nls
+import kotlin.math.min
+
+fun PythonPackageManager.launchReload() {
+ ApplicationManager.getApplication().coroutineScope.launch {
+ reloadPackages()
+ }
+}
+
+suspend fun PythonPackageManager.runPackagingTool(operation: String, arguments: List, @Nls text: String): String {
+ // todo[akniazev]: check for package management tools
+ val helpersAwareTargetRequest = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project)
+ val targetEnvironmentRequest = helpersAwareTargetRequest.targetEnvironmentRequest
+ val pythonExecution = prepareHelperScriptExecution(PythonHelper.PACKAGING_TOOL, helpersAwareTargetRequest)
+
+ // todo[akniazev]: check applyWorkingDir: PyTargetEnvironmentPackageManager.java:133
+
+ pythonExecution.addParameter(operation)
+
+ proxyString?.let {
+ pythonExecution.addParameter("--proxy")
+ pythonExecution.addParameter(it)
+ }
+
+ arguments.forEach(pythonExecution::addParameter)
+
+ // // todo[akniazev]: add extra args to package specification
+
+ val targetProgressIndicator = TargetProgressIndicator.EMPTY
+ val targetEnvironment = targetEnvironmentRequest.prepareEnvironment(targetProgressIndicator)
+
+ targetEnvironment.uploadVolumes.entries.forEach { (_, value) ->
+ value.upload(".", targetProgressIndicator)
+ }
+
+ val targetedCommandLine = pythonExecution.buildTargetedCommandLine(targetEnvironment, sdk, emptyList())
+
+ val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
+ // from targets package manager
+ // TODO [targets] Apply environment variables: setPythonUnbuffered(...), setPythonDontWriteBytecode(...), resetHomePathChanges(...)
+ // TODO [targets] Apply flavor from PythonSdkFlavor.getFlavor(mySdk)
+ // TODO [targets] check askForSudo
+
+ val process = targetEnvironment.createProcess(targetedCommandLine, indicator)
+
+ val commandLine = targetedCommandLine.collectCommandsSynchronously()
+ val commandLineString = commandLine.joinToString(" ")
+
+ thisLogger().debug("Running python packaging tool. Operation: $operation")
+ val handler = CapturingProcessHandler(process, targetedCommandLine.charset, commandLineString)
+
+ val result = withBackgroundProgressIndicator(project, text, cancellable = true) {
+ handler.runProcess(10 * 60 * 1000)
+ }
+
+ if (result.isCancelled) throw RunCanceledByUserException()
+ result.checkSuccess(thisLogger())
+ val exitCode = result.exitCode
+ val helperPath = commandLine.firstOrNull() ?: ""
+ val args: List = commandLine.subList(min(1, commandLine.size), commandLine.size)
+ if (exitCode != 0) {
+ val message = if (StringUtil.isEmptyOrSpaces(result.stdout) && StringUtil.isEmptyOrSpaces(result.stderr)) PySdkBundle.message(
+ "python.conda.permission.denied")
+ else PySdkBundle.message("python.sdk.packaging.non.zero.exit.code", exitCode)
+ throw PyExecutionException(message, helperPath, args, result)
+ }
+
+ if (result.isTimeout) {
+ throw PyExecutionException(PySdkBundle.message("python.sdk.packaging.timed.out"), helperPath, args, result)
+ }
+
+ return result.stdout
+}
+
+private val proxyString: String?
+ get() {
+ val settings = HttpConfigurable.getInstance()
+ if (settings != null && settings.USE_HTTP_PROXY) {
+ val credentials = if (settings.PROXY_AUTHENTICATION) "${settings.proxyLogin}:${settings.plainProxyPassword}@" else ""
+ return "http://$credentials${settings.PROXY_HOST}:${settings.PROXY_PORT}"
+ }
+ return null
+ }
+
+fun PythonRepositoryManager.packagesByRepository(): Sequence>> {
+ return repositories.asSequence().map { it to packagesFromRepository(it) }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackagesUpdater.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackagesUpdater.kt
new file mode 100644
index 000000000000..645cb94da978
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/PythonPackagesUpdater.kt
@@ -0,0 +1,39 @@
+// 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.management
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.startup.ProjectPostStartupActivity
+import com.intellij.util.io.exists
+import com.jetbrains.extensions.hasPython
+import com.jetbrains.python.packaging.PyPIPackageRanking
+import com.jetbrains.python.packaging.pip.PypiPackageCache
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.nio.file.Files
+import java.nio.file.Path
+import java.time.Duration
+import java.time.Instant
+
+class PythonPackagesUpdater : ProjectPostStartupActivity {
+
+ override suspend fun execute(project: Project) {
+ if (ApplicationManager.getApplication().isUnitTestMode || !project.hasPython) return
+ withContext(Dispatchers.IO) {
+ thisLogger().debug("Updating PyPI cache and ranking")
+ PyPIPackageRanking.reload()
+
+ if (PypiPackageCache.filePath.exists() && !cacheExpired(PypiPackageCache.filePath)) PypiPackageCache.loadFromFile()
+ else PypiPackageCache.refresh()
+ }
+ }
+
+ @Suppress("BlockingMethodInNonBlockingContext")
+ private fun cacheExpired(path: Path): Boolean {
+ val fileTime = Files.getLastModifiedTime(path)
+ val expirationTime = fileTime.toInstant().plus(Duration.ofDays(1))
+
+ return expirationTime.isBefore(Instant.now())
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/management/PythonRepositoryManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonRepositoryManager.kt
new file mode 100644
index 000000000000..5828001bfe11
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/management/PythonRepositoryManager.kt
@@ -0,0 +1,28 @@
+// 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.management
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.packaging.repository.PyPackageRepository
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+abstract class PythonRepositoryManager(val project: Project, val sdk: Sdk) {
+
+ abstract val repositories: List
+
+ abstract fun allPackages(): List
+
+ abstract fun packagesFromRepository(repository: PyPackageRepository): List
+ suspend fun addRepository(repository: PyPackageRepository) { TODO() }
+ suspend fun removeRepository(repository: PyPackageRepository) { TODO() }
+ abstract suspend fun getPackageDetails(pkg: PythonPackageSpecification): PythonPackageDetails
+
+ abstract suspend fun refreshCashes()
+
+ abstract suspend fun initCaches()
+
+ internal abstract fun buildPackageDetails(rawInfo: String, spec: PythonPackageSpecification): PythonPackageDetails
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipBasedPackageManager.kt b/python/src/com/jetbrains/python/packaging/pip/PipBasedPackageManager.kt
new file mode 100644
index 000000000000..4556bff4ed79
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/pip/PipBasedPackageManager.kt
@@ -0,0 +1,30 @@
+// 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.pip
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.packaging.management.runPackagingTool
+import com.jetbrains.python.packaging.common.PythonPackage
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+abstract class PipBasedPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
+ override suspend fun installPackage(specification: PythonPackageSpecification) {
+ withContext(Dispatchers.IO) {
+ runPackagingTool("install", specification.buildInstallationString(), PyBundle.message("python.packaging.install.progress", specification.name))
+ reloadPackages()
+ }
+ }
+
+ override suspend fun uninstallPackage(pkg: PythonPackage) {
+ withContext(Dispatchers.IO) {
+ runPackagingTool("uninstall", listOf(pkg.name), PyBundle.message("python.packaging.uninstall.progress", pkg.name))
+ reloadPackages()
+ }
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipBasedRepositoryManager.kt b/python/src/com/jetbrains/python/packaging/pip/PipBasedRepositoryManager.kt
new file mode 100644
index 000000000000..68523dfe8ae0
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/pip/PipBasedRepositoryManager.kt
@@ -0,0 +1,121 @@
+// 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.pip
+
+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.PyPIPackageUtil
+import com.jetbrains.python.packaging.PyPackageVersionComparator
+import com.jetbrains.python.packaging.cache.PythonSimpleRepositoryCache
+import com.jetbrains.python.packaging.common.EmptyPythonPackageDetails
+import com.jetbrains.python.packaging.management.PythonRepositoryManager
+import com.jetbrains.python.packaging.management.packagesByRepository
+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 com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
+import org.jetbrains.annotations.ApiStatus
+import java.time.Duration
+
+@ApiStatus.Experimental
+abstract class PipBasedRepositoryManager(project: Project, sdk: Sdk) : PythonRepositoryManager(project, sdk) {
+
+ override val repositories: List
+ get() = listOf(PyPIPackageRepository) + PythonSimpleRepositoryCache.repositories
+
+ private val gson = Gson()
+ private val packageDetailsCache = Caffeine.newBuilder()
+ .maximumSize(10)
+ .expireAfterWrite(Duration.ofHours(1))
+ .build {
+ // todo[akniazev] make it possible to show info from several repos
+ val repositoryUrl = it.repository?.repositoryUrl ?: PyPIPackageRepository.repositoryUrl!!
+ val result = runCatching {
+
+ val packageUrl = repositoryUrl.replace("simple", "pypi/${it.name}/json")
+ HttpRequests.request(packageUrl)
+ .withBasicAuthorization(it.repository)
+ .readTimeout(3000)
+ .readString()
+ }
+ if (result.isFailure) {
+ thisLogger().debug("Request failed for package $it.name")
+
+ val versions = tryParsingVersionsFromPage(it.name, repositoryUrl)
+ if (versions != null) return@build PythonSimplePackageDetails(it.name,
+ versions.sortedWith(PyPackageVersionComparator.STR_COMPARATOR.reversed()),
+ it.repository!!,
+ description = PyBundle.message("python.packaging.no.package.info"))
+ else return@build EmptyPythonPackageDetails(it.name, PyBundle.message("python.packages.request.failed"))
+ }
+
+ buildPackageDetails(result.getOrThrow(), it)
+ }
+
+
+ override fun buildPackageDetails(rawInfo: String, spec: PythonPackageSpecification): PythonPackageDetails {
+ try {
+ val packageDetails = gson.fromJson(rawInfo, PyPIPackageUtil.PackageDetails::class.java)
+ return PythonSimplePackageDetails(spec.name,
+ packageDetails.releases.sortedWith(PyPackageVersionComparator.STR_COMPARATOR.reversed()),
+ spec.repository!!,
+ packageDetails.info.summary,
+ packageDetails.info.description,
+ packageDetails.info.descriptionContentType,
+ packageDetails.info.projectUrls["Documentation"])
+
+ }
+ catch (ex: Exception) {
+ thisLogger().error(ex)
+ return EmptyPythonPackageDetails(spec.name, PyBundle.message("python.packaging.could.not.parse.response", spec.name, spec.repository?.name))
+ }
+ }
+
+ private fun tryParsingVersionsFromPage(name: String, repositoryUrl: String): List? {
+ val versions = runCatching {
+ val url = StringUtil.trimEnd(repositoryUrl, "/") + "/" + name
+ PyPIPackageUtil.parsePackageVersionsFromArchives(url, name)
+ }
+ return versions.getOrNull()
+ }
+
+
+ override suspend fun initCaches() {
+ if (PypiPackageCache.isEmpty()) {
+ PypiPackageCache.loadCache()
+ }
+
+ val service = service()
+ if (service.repositories.isNotEmpty() && PythonSimpleRepositoryCache.isEmpty()) {
+ PythonSimpleRepositoryCache.refresh()
+ }
+ }
+
+ override suspend fun refreshCashes() {
+ PypiPackageCache.refresh()
+ PythonSimpleRepositoryCache.refresh()
+ }
+
+ override fun allPackages(): List {
+ // todo[akniazev] check if it is even needed
+ return packagesByRepository().flatMap { it.second }.distinct().toList()
+ }
+
+ override fun packagesFromRepository(repository: PyPackageRepository): List {
+ return if (repository is PyPIPackageRepository) PypiPackageCache.packages
+ else PythonSimpleRepositoryCache[repository] ?: error("No packages for requested repository in cache")
+ }
+
+ override suspend fun getPackageDetails(pkg: PythonPackageSpecification): PythonPackageDetails {
+ return packageDetailsCache[pkg]
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPackagingToolwindowActionProvider.kt b/python/src/com/jetbrains/python/packaging/pip/PipPackagingToolwindowActionProvider.kt
new file mode 100644
index 000000000000..0ada465443eb
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/pip/PipPackagingToolwindowActionProvider.kt
@@ -0,0 +1,23 @@
+// 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.pip
+
+import com.jetbrains.python.PyBundle
+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.toolwindow.PythonPackageInstallAction
+import com.jetbrains.python.packaging.toolwindow.PythonPackagingToolwindowActionProvider
+import com.jetbrains.python.packaging.toolwindow.SimplePythonPackageInstallAction
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+class PipPackagingToolwindowActionProvider : PythonPackagingToolwindowActionProvider {
+ override fun getInstallActions(details: PythonPackageDetails, packageManager: PythonPackageManager): List? {
+ if (packageManager is PipPythonPackageManager && details is PythonSimplePackageDetails)
+ return listOf(SimplePythonPackageInstallAction(PyBundle.message("python.packaging.button.install.package"), packageManager.project))
+ return null
+ }
+}
+
+
+
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt
new file mode 100644
index 000000000000..b6c9646f550e
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt
@@ -0,0 +1,41 @@
+// 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.pip
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.management.runPackagingTool
+import com.jetbrains.python.packaging.common.PythonPackage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+class PipPythonPackageManager(project: Project, sdk: Sdk) : PipBasedPackageManager(project, sdk) {
+ override var installedPackages: List = emptyList()
+ private set
+
+ override val repositoryManager: PipRepositoryManager = PipRepositoryManager(project, sdk)
+
+ override suspend fun reloadPackages() {
+ withContext(Dispatchers.IO) {
+ val output = runPackagingTool("list", emptyList(), PyBundle.message("python.packaging.list.progress"))
+
+ val packages = output.lines().filter { it.isNotBlank() }.map {
+ val line = it.split("\t")
+ PythonPackage(line[0], line[1])
+ }
+
+ withContext(Dispatchers.Main) {
+ installedPackages = packages
+ }
+
+ ApplicationManager.getApplication()
+ .messageBus
+ .syncPublisher(PACKAGE_MANAGEMENT_TOPIC)
+ .packagesChanged(sdk)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipRepositoryManager.kt b/python/src/com/jetbrains/python/packaging/pip/PipRepositoryManager.kt
new file mode 100644
index 000000000000..d32daee1caf1
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/pip/PipRepositoryManager.kt
@@ -0,0 +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.pip
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+class PipRepositoryManager(project: Project, sdk: Sdk) : PipBasedRepositoryManager(project, sdk)
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/pip/PypiPackageCache.kt b/python/src/com/jetbrains/python/packaging/pip/PypiPackageCache.kt
new file mode 100644
index 000000000000..8e266f4e6750
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/pip/PypiPackageCache.kt
@@ -0,0 +1,97 @@
+// 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.pip
+
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.InstanceCreator
+import com.google.gson.reflect.TypeToken
+import com.intellij.openapi.application.PathManager
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.util.io.exists
+import com.jetbrains.python.packaging.PyPIPackageUtil
+import com.jetbrains.python.packaging.cache.PythonPackageCache
+import com.jetbrains.python.packaging.common.RANKING_AWARE_PACKAGE_NAME_COMPARATOR
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+import java.lang.reflect.Type
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.time.Duration
+import java.time.Instant
+import java.util.*
+
+@ApiStatus.Experimental
+object PypiPackageCache : PythonPackageCache {
+ private val gson: Gson = GsonBuilder()
+ .registerTypeAdapter(object : TypeToken>() {}.type,
+ object : InstanceCreator> {
+ override fun createInstance(type: Type?): TreeSet {
+ return TreeSet(RANKING_AWARE_PACKAGE_NAME_COMPARATOR)
+ }
+ })
+ .create()
+
+ override val packages: List
+ get() = cache.toList()
+
+ private var cache: TreeSet = TreeSet(RANKING_AWARE_PACKAGE_NAME_COMPARATOR)
+
+ val filePath: Path
+ get() = Paths.get(PathManager.getSystemPath(), "python_packages", "packages_v2.json")
+
+ suspend fun loadFromFile() {
+ withContext(Dispatchers.IO) {
+ val type = object : TypeToken>() {}.type
+ val newCache = Files.newBufferedReader(filePath, StandardCharsets.UTF_8).use { reader ->
+ val newCache: TreeSet = gson.fromJson(reader, type)
+ newCache
+ }
+ withContext(Dispatchers.Main) {
+ cache = newCache
+ }
+ }
+ }
+
+ suspend fun store() {
+ withContext(Dispatchers.IO) {
+ Files.createDirectories(filePath.parent)
+ Files.newBufferedWriter(filePath, StandardCharsets.UTF_8).use { writer ->
+ gson.toJson(cache, writer)
+ }
+ }
+ }
+
+ internal suspend fun loadCache() {
+ withContext(Dispatchers.IO) {
+ thisLogger().debug("Updating PyPI packages cache")
+ if (filePath.exists()) {
+ val fileTime = Files.getLastModifiedTime(filePath)
+ if (fileTime.toInstant().plus(Duration.ofDays(1)).isAfter(Instant.now())) {
+ thisLogger().debug("Cache file is not expired, reading packages locally")
+ loadFromFile()
+ return@withContext
+ }
+ thisLogger().debug("Cache expired, rebuilding it")
+ refresh()
+ return@withContext
+ }
+ thisLogger().debug("Cache file does not exist, reading packages from PyPI")
+ refresh()
+ }
+ }
+
+ suspend fun refresh() {
+ cache.clear()
+ withContext(Dispatchers.IO) {
+ cache.addAll(PyPIPackageUtil.parsePyPIListFromWeb(PyPIPackageUtil.PYPI_LIST_URL))
+ store()
+ }
+ }
+
+ override operator fun contains(key: String): Boolean = key in cache
+
+ override fun isEmpty(): Boolean = cache.isEmpty()
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/repository/PyPackageRepository.kt b/python/src/com/jetbrains/python/packaging/repository/PyPackageRepository.kt
index 53c2e7208cb9..8245369cb6cd 100644
--- a/python/src/com/jetbrains/python/packaging/repository/PyPackageRepository.kt
+++ b/python/src/com/jetbrains/python/packaging/repository/PyPackageRepository.kt
@@ -7,9 +7,9 @@ import com.intellij.credentialStore.generateServiceName
import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.openapi.components.BaseState
import com.intellij.util.xmlb.annotations.Transient
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import com.jetbrains.python.packaging.common.PythonSimplePackageSpecification
import org.jetbrains.annotations.ApiStatus
-import java.net.URLEncoder
-import java.nio.charset.StandardCharsets
@ApiStatus.Experimental
open class PyPackageRepository() : BaseState() {
@@ -57,4 +57,9 @@ open class PyPackageRepository() : BaseState() {
this.repositoryUrl = repositoryUrl
this.login = username
}
+
+ open fun createPackageSpecification(packageName: String,
+ version: String? = null): PythonPackageSpecification {
+ return PythonSimplePackageSpecification(packageName, version, this)
+ }
}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/repository/PyPackageRepositoryUtil.kt b/python/src/com/jetbrains/python/packaging/repository/PyPackageRepositoryUtil.kt
index aab29870be10..db10b184343b 100644
--- a/python/src/com/jetbrains/python/packaging/repository/PyPackageRepositoryUtil.kt
+++ b/python/src/com/jetbrains/python/packaging/repository/PyPackageRepositoryUtil.kt
@@ -10,7 +10,8 @@ import java.nio.charset.StandardCharsets
import java.util.*
@ApiStatus.Experimental
-internal fun RequestBuilder.withBasicAuthorization(repository: PyPackageRepository): RequestBuilder {
+internal fun RequestBuilder.withBasicAuthorization(repository: PyPackageRepository?): RequestBuilder {
+ if (repository == null) return this
val password = repository.getPassword()
if (repository.login != null && password != null) {
val credentials = Base64.getEncoder().encode("${repository.login}:${password}".toByteArray()).toString(StandardCharsets.UTF_8)
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagesToolWindowFactory.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagesToolWindowFactory.kt
index 200ee34e8280..c6e1174944bc 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagesToolWindowFactory.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagesToolWindowFactory.kt
@@ -11,7 +11,7 @@ import com.intellij.ui.content.ContentFactory
class PyPackagesToolWindowFactory : ToolWindowFactory, DumbAware {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val service = project.service()
- val toolWindowPanel = PyPackagingToolWindowPanel(service, toolWindow)
+ val toolWindowPanel = PyPackagingToolWindowPanel(project, toolWindow)
service.initialize(toolWindowPanel)
val content = ContentFactory.getInstance().createContent(toolWindowPanel.component, null, false)
toolWindow.contentManager.addContent(content)
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTablesView.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTablesView.kt
index 26f2ecc21302..fe883598a031 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTablesView.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTablesView.kt
@@ -2,6 +2,7 @@
package com.jetbrains.python.packaging.toolwindow
import com.intellij.icons.AllIcons
+import com.intellij.openapi.project.Project
import com.intellij.ui.JBColor
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.repository.PyPackageRepository
@@ -10,11 +11,13 @@ import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTable
-class PyPackagingTablesView(private val service: PyPackagingToolWindowService, private val container: JPanel) {
+class PyPackagingTablesView(private val project: Project,
+ private val container: JPanel,
+ private val controller: PyPackagingToolWindowPanel) {
private val repositories: MutableList> = mutableListOf()
private val installedPackages = PyPackagingTableGroup(
object : PyPackageRepository(message("python.toolwindow.packages.installed.label"), "", "") {},
- PyPackagesTable(PyPackagesTableModel(), service, this))
+ PyPackagesTable(project, PyPackagesTableModel(), this, controller))
private val invalidRepositories: MutableMap = mutableMapOf()
init {
installedPackages.addTo(container)
@@ -31,6 +34,7 @@ class PyPackagingTablesView(private val service: PyPackagingToolWindowService, p
table.expand()
}
+ // todo[akniazev]: selecting a package in 'installed' list might make more sense
tableToData
.firstOrNull { (_, data) -> data.exactMatch != -1 }
?.let { selectPackage(it.second) }
@@ -57,7 +61,7 @@ class PyPackagingTablesView(private val service: PyPackagingToolWindowService, p
if (existingRepo != null) existingRepo.items = withExpander
else {
- val newTable = PyPackagesTable(PyPackagesTableModel(), service, this)
+ val newTable = PyPackagesTable(project, PyPackagesTableModel(), this, controller)
newTable.items = withExpander
val newTableGroup = PyPackagingTableGroup(data.repository, newTable)
@@ -112,49 +116,8 @@ class PyPackagingTablesView(private val service: PyPackagingToolWindowService, p
.forEach { it.table.clearSelection() }
}
- fun packagesAdded(newPackages: List) {
- addInstalled(newPackages)
- repositories.forEach {
- val selectedRow = it.table.selectedRow
- it.table.clearSelection()
- newPackages.forEach { pkg ->
- val index = it.table.items.indexOfFirst { item -> item.name == pkg.name }
- if (index != -1) {
- it.replace(index, pkg)
- }
- }
-
- if (selectedRow != -1) {
- it.table.setRowSelectionInterval(selectedRow, selectedRow)
- }
- }
- }
-
private fun findTableForRepo(repository: PyPackageRepository) = repositories.find { it.name == repository.name }
- private fun addInstalled(newPackages: List) {
- installedPackages.table.addRows(newPackages)
- installedPackages.updateHeaderText(installedPackages.table.items.size)
- }
-
- fun packageDeleted(deletedPackage: DisplayablePackage) {
- val index = installedPackages.items.indexOfFirst { it.name == deletedPackage.name }
- if (index != -1) {
- installedPackages.table.removeRow(index)
- installedPackages.itemsCount?.let {
- installedPackages.updateHeaderText(it - 1)
- }
- }
-
- repositories.forEach { repo ->
- val repoIndex = repo.items.indexOfFirst { it.name == deletedPackage.name }
- if (repoIndex != -1) {
- repo.replace(repoIndex, InstallablePackage(deletedPackage.name, deletedPackage.repository))
- }
- }
-
- }
-
fun selectNextFrom(currentTable: JTable) {
val targetGroup = when (currentTable) {
installedPackages.table -> repositories.firstOrNull()
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt
index 569f3ca59f76..768b4d8c18ab 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt
@@ -3,10 +3,14 @@ package com.jetbrains.python.packaging.toolwindow
import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
+import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.impl.ActionButton
+import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
+import com.intellij.openapi.components.service
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
+import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.popup.JBPopupFactory
@@ -24,11 +28,19 @@ import com.intellij.ui.jcef.JCEFHtmlPanel
import com.intellij.util.Alarm
import com.intellij.util.Alarm.ThreadToUse
import com.intellij.util.SingleAlarm
+import com.intellij.util.childScope
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.NamedColorUtil
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle.message
+import com.jetbrains.python.packaging.common.PythonLocalPackageSpecification
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.common.PythonVcsPackageSpecification
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension
@@ -38,7 +50,12 @@ import java.awt.event.MouseEvent
import javax.swing.*
import javax.swing.event.DocumentEvent
-class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWindow: ToolWindow) : SimpleToolWindowPanel(false, true) {
+class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolWindow) : SimpleToolWindowPanel(false, true), Disposable {
+ private val packagingScope = ApplicationManager.getApplication().coroutineScope.childScope(Dispatchers.Default)
+ private var selectedPackage: DisplayablePackage? = null
+ private var selectedPackageDetails: PythonPackageDetails? = null
+
+ // UI elements
private val packageNameLabel = JLabel().apply { font = JBFont.h4().asBold(); isVisible = false }
private val versionLabel = JLabel().apply { isVisible = false }
private val documentationLink = HyperlinkLabel(message("python.toolwindow.packages.documentation.link")).apply {
@@ -53,7 +70,6 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
private val progressBar: JProgressBar
private val versionSelector: JBComboBoxLabel
private val descriptionPanel: JCEFHtmlPanel
- private var currentPackageInfo: PackageInfo? = null
private var documentationUrl: String? = null
private val packageListPanel: JPanel
@@ -81,6 +97,8 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
init {
+ val service = project.service()
+ Disposer.register(service, this)
withEmptyText(message("python.toolwindow.packages.no.interpreter.text"))
descriptionPanel = PyPackagingJcefHtmlPanel(service.project)
Disposer.register(toolWindow.disposable, descriptionPanel)
@@ -90,7 +108,7 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
isVisible = false
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
- val versions = listOf(latestText) + (currentPackageInfo?.availableVersions ?: emptyList())
+ val versions = listOf(latestText) + (selectedPackageDetails?.availableVersions ?: emptyList())
JBPopupFactory.getInstance().createListPopup(
object : BaseListPopupStep(null, versions) {
override fun onChosen(@NlsContexts.Label selectedValue: String, finalChoice: Boolean): PopupStep<*>? {
@@ -102,20 +120,22 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
})
}
- // todo: add options to install with args
- installButton = JBOptionButton(object : AbstractAction(message("python.toolwindow.packages.install.button")) {
- override fun actionPerformed(e: ActionEvent?) {
- val version = if (versionSelector.text == latestText) null else versionSelector.text
- service.installSelectedPackage(version)
- }
- }, null).apply { isVisible = false }
-
+ // todo[akniazev]: add options to install with args
+ installButton = JBOptionButton(null, null).apply { isVisible = false }
val uninstallToolbar = ActionManager.getInstance()
.createActionToolbar(ActionPlaces.TOOLWINDOW_CONTENT, DefaultActionGroup(DefaultActionGroup().apply {
add(object : AnAction(message("python.toolwindow.packages.delete.package")) {
override fun actionPerformed(e: AnActionEvent) {
- service.deleteSelectedPackage()
+ if (selectedPackage is InstalledPackage) {
+ packagingScope.launch(Dispatchers.Main) {
+ startProgress()
+ withContext(Dispatchers.IO) {
+ service.deletePackage(selectedPackage as InstalledPackage)
+ }
+ stopProgress()
+ }
+ } else error("Trying to delete package, that is not InstalledPackage")
}
})
isPopup = true
@@ -124,7 +144,7 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
icon = AllIcons.Actions.More
}
}), true)
- uninstallToolbar.setTargetComponent(this)
+ uninstallToolbar.targetComponent = this
uninstallAction = uninstallToolbar.component
progressBar = JProgressBar(JProgressBar.HORIZONTAL).apply {
@@ -141,7 +161,7 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
background = UIUtil.getListBackground()
}
- tablesView = PyPackagingTablesView(service, packageListPanel)
+ tablesView = PyPackagingTablesView(project, packageListPanel, this)
leftPanel = ScrollPaneFactory.createScrollPane(packageListPanel, true)
rightPanel = borderPanel {
@@ -219,18 +239,20 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
}
})
val actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLWINDOW_CONTENT, actionGroup,true)
- actionToolbar.setTargetComponent(this)
+ actionToolbar.targetComponent = this
val installFromLocationLink = DropDownLink(message("python.toolwindow.packages.add.package.action"),
listOf(fromVcsText, fromDiscText)) {
- val params = when (it) {
+ val specification = when (it) {
fromDiscText -> showInstallFromDiscDialog(service)
fromVcsText -> showInstallFromVcsDialog(service)
else -> throw IllegalStateException("Unknown operation")
}
- if (params != null) {
- service.installFromLocation(params.first, params.second)
+ if (specification != null) {
+ packagingScope.launch {
+ service.installPackage(specification)
+ }
}
}
@@ -241,7 +263,7 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
minimumSize = Dimension(minimumSize.width, 30)
maximumSize = Dimension(maximumSize.width, 30)
add(searchTextField)
- actionToolbar.component.maximumSize = Dimension(60, actionToolbar.component.maximumSize.height)
+ actionToolbar.component.maximumSize = Dimension(70, actionToolbar.component.maximumSize.height)
add(actionToolbar.component)
add(installFromLocationLink)
}
@@ -251,7 +273,7 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
setContent(mainPanel!!)
}
- private fun showInstallFromVcsDialog(service: PyPackagingToolWindowService): Pair? {
+ private fun showInstallFromVcsDialog(service: PyPackagingToolWindowService): PythonVcsPackageSpecification? {
var editable = false
var link = ""
val systems = listOf(message("python.toolwindow.packages.add.package.vcs.git"),
@@ -284,12 +306,13 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
message("python.toolwindow.packages.add.package.vcs.bzr") -> "bzr+"
else -> throw IllegalStateException("Unknown VCS")
}
- return Pair(prefix + link, editable)
+
+ return PythonVcsPackageSpecification(link, link, prefix, editable)
}
return null
}
- private fun showInstallFromDiscDialog(service: PyPackagingToolWindowService): Pair? {
+ private fun showInstallFromDiscDialog(service: PyPackagingToolWindowService): PythonLocalPackageSpecification? {
var editable = false
val textField = TextFieldWithBrowseButton().apply {
@@ -309,7 +332,7 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
}
val shouldInstall = dialog(message("python.toolwindow.packages.add.package.dialog.title"), panel, project = service.project, resizable = true).showAndGet()
- return if (shouldInstall) Pair("file://${textField.text}", editable) else null
+ return if (shouldInstall) PythonLocalPackageSpecification(textField.text, textField.text, editable) else null
}
private fun trackOrientation(service: PyPackagingToolWindowService) {
@@ -330,6 +353,64 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
})
}
+
+
+ fun packageSelected(selectedPackage: DisplayablePackage) {
+ showHeaderForPackage(selectedPackage)
+ this.selectedPackage = selectedPackage
+ packagingScope.launch(Dispatchers.IO) {
+ val service = project.service()
+ val packageDetails = service.detailsForPackage(selectedPackage)
+
+ val installActions = PythonPackagingToolwindowActionProvider.EP_NAME
+ .extensionList
+ .firstNotNullOf {
+ it.getInstallActions(packageDetails, service.manager)
+ }
+
+ withContext(Dispatchers.Main) {
+ selectedPackageDetails = packageDetails
+
+ if (splitter?.secondComponent != rightPanel) {
+ splitter!!.secondComponent = rightPanel
+ }
+
+ val renderedDescription = with(packageDetails) {
+ when {
+ !description.isNullOrEmpty() -> service.convertToHTML(descriptionContentType, description!!)
+ !summary.isNullOrEmpty() -> service.wrapHtml(summary!!)
+ else -> NO_DESCRIPTION
+ }
+ }
+
+ descriptionPanel.setHtml(renderedDescription)
+ documentationUrl = packageDetails.documentationUrl
+ documentationLink.isVisible = documentationUrl != null
+
+
+ installButton.action = wrapAction(installActions.first(), packageDetails)
+ if (installActions.size > 1) {
+ installButton.options = installActions
+ .asSequence().drop(1).map { wrapAction(it, packageDetails) }.toList().toTypedArray()
+ }
+
+ installButton.repaint()
+ }
+ }
+ }
+
+ private fun wrapAction(installAction: PythonPackageInstallAction, details: PythonPackageDetails): Action {
+ return object : AbstractAction(installAction.text) {
+ override fun actionPerformed(e: ActionEvent?) {
+ val version = if (versionSelector.text == latestText) null else versionSelector.text
+ packagingScope.launch(Dispatchers.IO) {
+ val specification = details.toPackageSpecification(version)
+ installAction.installPackage(specification)
+ }
+ }
+ }
+ }
+
fun showSearchResult(installed: List, repoData: List) {
tablesView.showSearchResult(installed, repoData)
}
@@ -338,18 +419,6 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
tablesView.resetSearch(installed, repos)
}
- fun displaySelectedPackageInfo(packageInfo: PackageInfo) {
- currentPackageInfo = packageInfo
- if (splitter?.secondComponent != rightPanel) {
- splitter!!.secondComponent = rightPanel
- }
-
- descriptionPanel.setHtml(packageInfo.description)
- documentationUrl = packageInfo.documentationUrl
- documentationLink.isVisible = documentationUrl != null
- }
-
-
fun startProgress() {
progressBar.isVisible = true
hideInstallableControls()
@@ -385,20 +454,13 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
versionSelector.isVisible = false
}
- fun packageInstalled(newFiltered: List) {
- tablesView.packagesAdded(newFiltered)
- }
-
- fun packageDeleted(deletedPackage: DisplayablePackage) {
- tablesView.packageDeleted(deletedPackage)
- }
-
- fun showHeaderForPackage(selectedPackage: DisplayablePackage) {
+ private fun showHeaderForPackage(selectedPackage: DisplayablePackage) {
packageNameLabel.text = selectedPackage.name
packageNameLabel.isVisible = true
documentationLink.isVisible = false
if (selectedPackage is InstalledPackage) {
+ @Suppress("HardCodedStringLiteral")
versionLabel.text = selectedPackage.instance.version
showInstalledControls()
}
@@ -415,16 +477,14 @@ class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWind
splitter?.secondComponent = noPackagePanel
}
+ override fun dispose() {
+ packagingScope.cancel()
+ }
companion object {
private const val HORIZONTAL_SPLITTER_KEY = "Python.PackagingToolWindow.Horizontal"
private const val VERTICAL_SPLITTER_KEY = "Python.PackagingToolWindow.Vertical"
-
- val REMOTE_INTERPRETER_TEXT: String
- get() = message("python.toolwindow.packages.remote.interpreter.placeholder")
- val REQUEST_FAILED_TEXT: String
- get() = message("python.toolwindow.packages.request.failed")
val NO_DESCRIPTION: String
get() = message("python.toolwindow.packages.no.description.placeholder")
}
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
index 06cb9703926f..cf9d63e01e14 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
@@ -1,19 +1,15 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
-import com.google.gson.Gson
import com.intellij.ProjectTopics
-import com.intellij.execution.ExecutionException
+import com.intellij.notification.NotificationGroupManager
+import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
-import com.intellij.openapi.application.AppUIExecutor
-import com.intellij.openapi.application.impl.coroutineDispatchingContext
+import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.options.ex.SingleConfigurableEditor
-import com.intellij.openapi.progress.ProgressIndicator
-import com.intellij.openapi.progress.ProgressManager
-import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.withBackgroundProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
@@ -21,282 +17,179 @@ import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.util.io.HttpRequests
-import com.intellij.webcore.packaging.PackageManagementService
-import com.intellij.webcore.packaging.RepoPackage
+import com.intellij.util.childScope
import com.jetbrains.python.PyBundle.*
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.packaging.*
-import com.jetbrains.python.packaging.PyPackageVersionComparator.STR_COMPARATOR
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import com.jetbrains.python.packaging.common.PythonPackageManagementListener
+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.ui.PyPackageManagementService
import com.jetbrains.python.sdk.PySdkUtil
-import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.statistics.modules
import kotlinx.coroutines.*
import org.intellij.plugins.markdown.ui.preview.html.MarkdownUtil
+import org.jetbrains.annotations.Nls
+import kotlin.Comparator
@Service
class PyPackagingToolWindowService(val project: Project) : Disposable {
private lateinit var toolWindowPanel: PyPackagingToolWindowPanel
+ lateinit var manager: PythonPackageManager
private var installedPackages: List = emptyList()
internal var currentSdk: Sdk? = null
- private var selectedPackage: DisplayablePackage? = null
- private var selectedInfo: PackageInfo? = null
- private var currentJob: Job? = null
private var searchJob: Job? = null
private var currentQuery: String = ""
- private val gson = Gson()
+ private val serviceScope = ApplicationManager.getApplication().coroutineScope.childScope(Dispatchers.Default)
+ private val invalidRepositories: List
+ get() = service().invalidRepositories.map(::PyInvalidRepositoryViewData)
fun initialize(toolWindowPanel: PyPackagingToolWindowPanel) {
this.toolWindowPanel = toolWindowPanel
- initForSdk(project.modules.firstOrNull()?.pythonSdk)
- GlobalScope.launch(Dispatchers.IO) {
- val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk) as PyPackageManagementService
- managementService.allPackages // load packages
- handleSearch("")
+ serviceScope.launch(Dispatchers.IO) {
+ PyPIPackageRanking.reload()
+ initForSdk(project.modules.firstOrNull()?.pythonSdk)
}
subscribeToChanges()
}
- fun packageSelected(pkg: DisplayablePackage) {
- currentJob?.cancel()
- selectedPackage = pkg
- toolWindowPanel.showHeaderForPackage(pkg)
- currentJob = GlobalScope.launch(Dispatchers.Default) {
- if (PythonSdkUtil.isRemote(currentSdk)) {
- selectedInfo = REMOTE_INTERPRETER_INFO
- }
- else {
- val response = fetchPackageInfo(selectedPackage!!)
- if (response != null) {
- val packageDetails = gson.fromJson(response, PyPIPackageUtil.PackageDetails::class.java)
-
- selectedInfo = with(packageDetails.info) {
- val renderedDescription = when {
- description.isNotEmpty() -> convertToHTML(descriptionContentType, description)
- summary.isNotEmpty() -> wrapHtml(summary)
- else -> PyPackagingToolWindowPanel.NO_DESCRIPTION
- }
- PackageInfo(projectUrls["Documentation"],
- renderedDescription,
- packageDetails.releases.sortedWith(STR_COMPARATOR.reversed()))
- }
- }
- else {
- // try to get version from repository
- val versionFromRepo = fetchVersionsFromPage(selectedPackage!!)
- if (versionFromRepo.isNotEmpty()) {
- selectedInfo = PackageInfo(null,
- wrapHtml("${message("python.toolwindow.packages.no.documentation")}
"),
- versionFromRepo)
- }
- else {
- selectedInfo = EMPTY_INFO
- }
- }
- }
-
- if (isActive) {
- withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
- toolWindowPanel.displaySelectedPackageInfo(selectedInfo!!)
- }
- }
- }
+ suspend fun detailsForPackage(selectedPackage: DisplayablePackage): PythonPackageDetails {
+ val spec = selectedPackage.repository.createPackageSpecification(selectedPackage.name)
+ return manager.repositoryManager.getPackageDetails(spec)
}
+
fun handleSearch(query: String) {
currentQuery = query
if (query.isNotEmpty()) {
searchJob?.cancel()
- searchJob = GlobalScope.launch(Dispatchers.Default) {
+ searchJob = serviceScope.launch {
val installed = installedPackages.filter { StringUtil.containsIgnoreCase(it.name, query) }
- val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk) as PyPackageManagementService
- val invalidRepositories = service().invalidRepositories.map(::PyInvalidRepositoryViewData)
- val packagesFromRepos = managementService.allPackagesByRepository.map {
- filterPackagesForRepo(it.value, query, it.key)
- }
+ val packagesFromRepos = manager.repositoryManager.packagesByRepository().map {
+ filterPackagesForRepo(it.second, query, it.first)
+ }.toList()
if (isActive) {
- withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
+ withContext(Dispatchers.Main) {
toolWindowPanel.showSearchResult(installed, packagesFromRepos + invalidRepositories)
}
}
}
}
else {
- val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk) as PyPackageManagementService
- val packagesFromRepos = managementService.allPackagesByRepository.map { entry ->
- val repository = service()
- .repositories
- .find { repo -> repo.repositoryUrl == entry.key } ?: PyPIPackageRepository
+ val packagesByRepository = manager.repositoryManager.packagesByRepository().map { (repository, packages) ->
+ val shownPackages = packages.asSequence().limitDisplayableResult(repository)
+ PyPackagesViewData(repository, shownPackages, moreItems = packages.size - PACKAGES_LIMIT)
+ }.toList()
- val (packagesSeq, size) = when {
- PyPIPackageUtil.isPyPIRepository(entry.key) -> Pair(PyPIPackageRanking.names, PyPIPackageCache.getInstance().packageNames.size)
- else -> Pair(entry.value.asSequence().map { it.name }, entry.value.size)
- }
-
- val shownPackages = packagesSeq.limitDisplayableResult(repository)
- PyPackagesViewData(repository, shownPackages, moreItems = size - PACKAGES_LIMIT)
- }
- val invalidRepositories = service().invalidRepositories.map(::PyInvalidRepositoryViewData)
-
- toolWindowPanel.resetSearch(installedPackages, packagesFromRepos + invalidRepositories)
+ toolWindowPanel.resetSearch(installedPackages, packagesByRepository + invalidRepositories)
}
}
- fun installSelectedPackage(version: String?) {
- val toInstall = selectedPackage as? InstallablePackage ?: return
- val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk)
- val listener = object : PackageManagementService.Listener {
- override fun operationStarted(packageName: String?) {
- toolWindowPanel.startProgress()
- }
+ suspend fun installPackage(specification: PythonPackageSpecification) {
+ manager.installPackage(specification)
- override fun operationFinished(packageName: String?, errorDescription: PackageManagementService.ErrorDescription?) {
- toolWindowPanel.stopProgress()
- collectInstalledPackages { newPackages ->
- val withRepo = newPackages.map { InstalledPackage(it.instance, toInstall.repository) }
- installedPackages = installedPackages.filterNot { it in newPackages } + withRepo
-
- if (currentQuery.isNotEmpty()) {
- val newFiltered = withRepo.filter { StringUtil.containsIgnoreCase(it.name, currentQuery) }
- toolWindowPanel.packageInstalled(newFiltered)
- }
- else {
- toolWindowPanel.packageInstalled(withRepo)
- }
-
- }
- }
- }
-
- managementService.installPackage(RepoPackage(toInstall.name, toInstall.repository.urlForInstallation), version, false, null, listener, false)
+ showPackagingNotification(message("python.packaging.notification.installed", specification.name))
}
- fun deleteSelectedPackage() {
- val packageToDelete = selectedPackage as? InstalledPackage ?: return
- val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk)
- val listener = object : PackageManagementService.Listener {
- override fun operationStarted(packageName: String?) {
- toolWindowPanel.startProgress()
- }
-
- override fun operationFinished(packageName: String?, errorDescription: PackageManagementService.ErrorDescription?) {
- toolWindowPanel.stopProgress()
- collectInstalledPackages {
- toolWindowPanel.packageDeleted(packageToDelete)
- if (packageToDelete.name == selectedPackage?.name) {
- selectedPackage = null
- toolWindowPanel.setEmpty()
- }
- }
- }
- }
- managementService.uninstallPackages(listOf(packageToDelete.instance), listener)
+ suspend fun deletePackage(selectedPackage: InstalledPackage) {
+ manager.uninstallPackage(selectedPackage.instance)
+ showPackagingNotification(message("python.packaging.notification.deleted", selectedPackage.name))
}
- private fun initForSdk(sdk: Sdk?) {
+ private suspend fun initForSdk(sdk: Sdk?) {
val previousSdk = currentSdk
currentSdk = sdk
if (currentSdk != null) {
- collectInstalledPackages(resetSearch = true)
+ manager = PythonPackageManager.forSdk(project, currentSdk!!) ?: error("No packages manager found for sdk: ${sdk?.name}")
+ manager.repositoryManager.initCaches()
+ manager.reloadPackages()
+ refreshInstalledPackages()
+ withContext(Dispatchers.Main) {
+ handleSearch("")
+ }
}
- toolWindowPanel.contentVisible = currentSdk != null
- if (currentSdk == null || currentSdk != previousSdk) {
- selectedPackage = null
- toolWindowPanel.setEmpty()
+ withContext(Dispatchers.Main) {
+ toolWindowPanel.contentVisible = currentSdk != null
+ if (currentSdk == null || currentSdk != previousSdk) {
+ toolWindowPanel.setEmpty()
+ }
}
}
private fun subscribeToChanges() {
val connection = project.messageBus.connect(this)
- connection.subscribe(PyPackageManager.PACKAGE_MANAGER_TOPIC, PyPackageManager.Listener {
- if (currentSdk == it) collectInstalledPackages(resetSearch = currentQuery.isEmpty())
+ connection.subscribe(PythonPackageManager.PACKAGE_MANAGEMENT_TOPIC, object : PythonPackageManagementListener {
+ override fun packagesChanged(sdk: Sdk) {
+ if (currentSdk == sdk) serviceScope.launch(Dispatchers.IO) {
+ refreshInstalledPackages()
+ withContext(Dispatchers.Main) {
+ handleSearch(currentQuery)
+ }
+ }
+ }
})
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) {
- initForSdk(project.modules.firstOrNull()?.pythonSdk)
+ serviceScope.launch(Dispatchers.IO) {
+ initForSdk(project.modules.firstOrNull()?.pythonSdk)
+ }
}
})
}
- private fun collectInstalledPackages(resetSearch: Boolean = false, callback: ((List) -> Unit)? = null) {
- val task = object : Task.Backgroundable(project, message("python.toolwindow.packages.collecting.packages.task.title")) {
- val sdk = currentSdk!!
- val previouslyInstalled = installedPackages
- var packages: List? = null
- var newPackages: List? = null
- override fun run(indicator: ProgressIndicator) {
- val currentlyInstalled = previouslyInstalled.mapTo(HashSet()) { it.name }
- packages = PyPackageManagers.getInstance().forSdk(sdk).refreshAndGetPackages(false).map {
- val repository = previouslyInstalled.find { pkg -> pkg.name == it.name }?.repository ?: PyEmptyPackagePackageRepository
- InstalledPackage(it, repository)
- }
- newPackages = packages?.filter { it.name !in currentlyInstalled }
- }
- override fun onSuccess() {
- installedPackages = packages ?: error("No installed packages found")
- if (resetSearch) handleSearch("")
- callback?.invoke(newPackages ?: emptyList())
- }
+ suspend fun refreshInstalledPackages() {
+ val packages = manager.installedPackages.map {
+ val repository = installedPackages.find { pkg -> pkg.name == it.name }?.repository ?: PyEmptyPackagePackageRepository
+ InstalledPackage(it, repository)
+ }
+
+ withContext(Dispatchers.Main) {
+ installedPackages = packages
}
- ProgressManager.getInstance().run(task)
}
- private suspend fun fetchPackageInfo(pkg: DisplayablePackage): String? = withContext(Dispatchers.IO) {
- val result = runCatching {
- val repoUrl = pkg.repository.repositoryUrl.let { if (it.isNullOrEmpty()) PyPIPackageUtil.PYPI_LIST_URL else it }
- val packageUrl = repoUrl.replace("simple", "pypi/${pkg.name}/json")
- HttpRequests.request(packageUrl)
- .withBasicAuthorization(pkg.repository)
- .readTimeout(3000)
- .readString()
+ private suspend fun showPackagingNotification(text: @Nls String) {
+ val notification = NotificationGroupManager.getInstance()
+ .getNotificationGroup("PythonPackages")
+ .createNotification(text, NotificationType.INFORMATION)
+
+ withContext(Dispatchers.Main) {
+ notification.notify(project)
}
- if (result.isFailure) thisLogger().debug("Request failed for package $pkg.name")
- result.getOrNull()
}
- private fun filterPackagesForRepo(packageNames: List,
+ private fun filterPackagesForRepo(packageNames: List,
query: String,
- url: String,
+ repository: PyPackageRepository,
skipItems: Int = 0): PyPackagesViewData {
- val comparator = createComparator(query, url)
-
- val repository = service().findByUrl(url) ?: PyPIPackageRepository
+ val comparator = createNameComparator(query, repository.repositoryUrl ?: "")
val searchResult = packageNames.asSequence()
- .filter { StringUtil.containsIgnoreCase(it.name, query) }
+ .filter { StringUtil.containsIgnoreCase(it, query) }
.toList()
val shownPackages = searchResult.asSequence()
.sortedWith(comparator)
- .map { it.name }
.limitDisplayableResult(repository, skipItems)
-
val exactMatch = shownPackages.indexOfFirst { StringUtil.equalsIgnoreCase(it.name, query) }
return PyPackagesViewData(repository, shownPackages, exactMatch, searchResult.size - shownPackages.size)
}
-
- private suspend fun fetchVersionsFromPage(pkg: DisplayablePackage): List = withContext(Dispatchers.IO) {
- val result = runCatching {
- val url = StringUtil.trimEnd(pkg.repository.repositoryUrl!!, "/") + "/" + pkg.name
- PyPIPackageUtil.parsePackageVersionsFromArchives(url, pkg.name)
- }
- return@withContext result.getOrDefault(emptyList()).sortedWith(STR_COMPARATOR.reversed())
- }
-
- private fun convertToHTML(contentType: String, description: String): String {
- return when (contentType) {
- "text/markdown" -> markdownToHtml(description, currentSdk!!.homeDirectory!!, project)
- "text/x-rst", "" -> rstToHtml(description, currentSdk!!)
- else -> description
+ suspend fun convertToHTML(contentType: String?, description: String): String {
+ return withContext(Dispatchers.IO) {
+ when (contentType) {
+ "text/markdown" -> markdownToHtml(description, currentSdk!!.homeDirectory!!, project)
+ "text/x-rst", "" -> rstToHtml(description, currentSdk!!)
+ else -> description
+ }
}
}
@@ -315,18 +208,20 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
}
override fun dispose() {
- currentJob?.cancel()
searchJob?.cancel()
+ serviceScope.cancel()
}
- private fun wrapHtml(html: String): String = "$html
"
+ fun wrapHtml(html: String): String = "$html
"
fun reloadPackages() {
- GlobalScope.launch(Dispatchers.IO) {
+ serviceScope.launch(Dispatchers.IO) {
withBackgroundProgressIndicator(project, message("python.packaging.loading.packages.progress.text"), cancellable = false) {
- val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk)
- PyPIPackageUtil.INSTANCE.loadAdditionalPackages(managementService.allRepositories!!, true)
- withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
+ manager.reloadPackages()
+ manager.repositoryManager.refreshCashes()
+ refreshInstalledPackages()
+
+ withContext(Dispatchers.Main) {
handleSearch("")
}
}
@@ -336,7 +231,7 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
fun manageRepositories() {
val updated = SingleConfigurableEditor(project, PyRepositoriesList(project)).showAndGet()
if (updated) {
- GlobalScope.launch(Dispatchers.IO) {
+ serviceScope.launch(Dispatchers.IO) {
val packageService = PyPackageService.getInstance()
val repositoryService = service()
val allRepos = repositoryService.repositories.map { it.repositoryUrl }
@@ -360,42 +255,14 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
}
fun getMoreResultsForRepo(repository: PyPackageRepository, skipItems: Int): PyPackagesViewData {
- val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk) as PyPackageManagementService
- val fromCurrentRepo = managementService.allPackagesByRepository[repository.repositoryUrl]!!
+ val packagesFromRepository = manager.repositoryManager.packagesFromRepository(repository)
+
if (currentQuery.isNotEmpty()) {
- return filterPackagesForRepo(fromCurrentRepo, currentQuery, repository.repositoryUrl!!, skipItems)
+ return filterPackagesForRepo(packagesFromRepository, currentQuery, repository, skipItems)
}
else {
- // The number of items to skip might be more than the number of ranked packages we store,
- // so we need to include the remaining packages from pypi, filtering out those, that are already shown.
- if (PyPIPackageUtil.isPyPIRepository(repository.repositoryUrl)) {
- val ranked = PyPIPackageRanking.packageRank
- val names = PyPIPackageRanking.names
- val rankedSize = PyPIPackageRanking.packageRank.size
- val pypiAdjusted = when {
- skipItems > rankedSize -> fromCurrentRepo.asSequence().drop(skipItems - rankedSize).map { it.name }
- skipItems + PACKAGES_LIMIT > rankedSize -> {
- val pypiRemaining = fromCurrentRepo.asSequence()
- .map { it.name }
- .filterNot { it in ranked }
-
- names.drop(skipItems) + pypiRemaining
- }
- else -> names.drop(skipItems)
- }
-
-
- val pypiPackages = pypiAdjusted.limitDisplayableResult(repository)
-
- val packageNum = PyPIPackageCache.getInstance().packageNames.size
- return PyPackagesViewData(repository, pypiPackages, moreItems = packageNum - PACKAGES_LIMIT)
- }
-
- val packagesFromRepo = fromCurrentRepo.asSequence()
- .map { it.name }
- .limitDisplayableResult(repository, skipItems)
-
- return PyPackagesViewData(repository, packagesFromRepo, moreItems = fromCurrentRepo.size - (PACKAGES_LIMIT + skipItems))
+ val packagesFromRepo = packagesFromRepository.asSequence().limitDisplayableResult(repository, skipItems)
+ return PyPackagesViewData(repository, packagesFromRepo, moreItems = packagesFromRepository.size - (PACKAGES_LIMIT + skipItems))
}
}
@@ -406,30 +273,11 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
.toList()
}
- fun installFromLocation(location: String, editable: Boolean) {
- val ui = PyPackageManagerUI(project, currentSdk!!, object : PyPackageManagerUI.Listener {
- override fun started() {}
-
- override fun finished(exceptions: MutableList?) {
- handleSearch("")
- }
- })
- val installOptions = if (editable) listOf("-e", location) else listOf(location)
-
- ui.install(listOf(PyRequirementImpl(location, emptyList(), installOptions, "")), emptyList())
- }
-
-
companion object {
- private val EMPTY_INFO = PackageInfo(null, PyPackagingToolWindowPanel.REQUEST_FAILED_TEXT, emptyList())
- private val REMOTE_INTERPRETER_INFO = PackageInfo(null, PyPackagingToolWindowPanel.REMOTE_INTERPRETER_TEXT, emptyList())
private const val PACKAGES_LIMIT = 50
-
- private fun createComparator(query: String, url: String): Comparator {
- val nameComparator = Comparator { o1, o2 ->
- val name1 = o1.name.toLowerCase()
- val name2 = o2.name.toLowerCase()
- val queryLowerCase = query.toLowerCase()
+ private fun createNameComparator(query: String, url: String): Comparator {
+ val nameComparator = Comparator { name1, name2 ->
+ val queryLowerCase = query.lowercase()
return@Comparator when {
name1.startsWith(queryLowerCase) && name2.startsWith(queryLowerCase) -> name1.length - name2.length
name1.startsWith(queryLowerCase) -> -1
@@ -441,8 +289,8 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
if (PyPIPackageUtil.isPyPIRepository(url)) {
val ranking = PyPIPackageRanking.packageRank
return Comparator { p1, p2 ->
- val rank1 = ranking[p1.name.toLowerCase()]
- val rank2 = ranking[p2.name.toLowerCase()]
+ val rank1 = ranking[p1.lowercase()]
+ val rank2 = ranking[p2.lowercase()]
return@Comparator when {
rank1 != null && rank2 == null -> -1
rank1 == null && rank2 != null -> 1
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions.kt
new file mode 100644
index 000000000000..f9c124a1da20
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions.kt
@@ -0,0 +1,31 @@
+// 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.toolwindow
+
+import com.intellij.openapi.components.service
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.project.Project
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import org.jetbrains.annotations.Nls
+
+interface PythonPackagingToolwindowActionProvider {
+ fun getInstallActions(details: PythonPackageDetails, packageManager: PythonPackageManager): List?
+
+ companion object {
+ val EP_NAME = ExtensionPointName.create("Pythonid.PythonPackagingToolwindowActionProvider")
+ }
+}
+
+abstract class PythonPackageInstallAction(internal val text: @Nls String,
+ internal val project: Project) {
+
+ abstract suspend fun installPackage(specification: PythonPackageSpecification)
+}
+
+class SimplePythonPackageInstallAction(text: @Nls String,
+ project: Project) : PythonPackageInstallAction(text, project) {
+ override suspend fun installPackage(specification: PythonPackageSpecification) {
+ project.service().installPackage(specification)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/modelComponents.kt b/python/src/com/jetbrains/python/packaging/toolwindow/modelComponents.kt
index b6ee8ecc599d..f47a86871072 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/modelComponents.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/modelComponents.kt
@@ -2,15 +2,12 @@
package com.jetbrains.python.packaging.toolwindow
import com.intellij.openapi.util.NlsSafe
-import com.jetbrains.python.packaging.PyPackage
import com.jetbrains.python.packaging.repository.PyPackageRepository
+import com.jetbrains.python.packaging.common.PythonPackage
sealed class DisplayablePackage(@NlsSafe val name: String, val repository: PyPackageRepository)
-class InstalledPackage(val instance: PyPackage, repository: PyPackageRepository) : DisplayablePackage(instance.name, repository)
+class InstalledPackage(val instance: PythonPackage, repository: PyPackageRepository) : DisplayablePackage(instance.name, repository)
class InstallablePackage(name: String, repository: PyPackageRepository) : DisplayablePackage(name, repository)
class ExpandResultNode(var more: Int, repository: PyPackageRepository) : DisplayablePackage("", repository)
-
-class PackageInfo(val documentationUrl: String?, @NlsSafe val description: String, val availableVersions: List)
-
open class PyPackagesViewData(@NlsSafe val repository: PyPackageRepository, val packages: List, val exactMatch: Int = -1, val moreItems: Int = 0)
class PyInvalidRepositoryViewData(repository: PyPackageRepository) : PyPackagesViewData(repository, emptyList())
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/uiComponents.kt b/python/src/com/jetbrains/python/packaging/toolwindow/uiComponents.kt
index 0f809c69a4a0..88d14ec58cc6 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/uiComponents.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/uiComponents.kt
@@ -2,6 +2,8 @@
package com.jetbrains.python.packaging.toolwindow
import com.intellij.icons.AllIcons
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.SideBorder
@@ -24,9 +26,13 @@ import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.TableCellRenderer
-internal class PyPackagesTable(model: ListTableModel, service: PyPackagingToolWindowService, tablesView: PyPackagingTablesView) : JBTable(model) {
+internal class PyPackagesTable(project: Project,
+ model: ListTableModel,
+ tablesView: PyPackagingTablesView,
+ controller: PyPackagingToolWindowPanel) : JBTable(model) {
private var lastSelectedRow = -1
init {
+ val service = project.service()
setShowGrid(false)
setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
val column = columnModel.getColumn(1)
@@ -42,7 +48,7 @@ internal class PyPackagesTable(model: ListTableModel,
lastSelectedRow = selectedRow
tablesView.requestSelection(this)
val pkg = model.items[selectedRow]
- if (pkg !is ExpandResultNode) service.packageSelected(pkg)
+ if (pkg !is ExpandResultNode) controller.packageSelected(pkg)
}
}
@@ -179,7 +185,7 @@ fun borderPanel(init: JPanel.() -> Unit) = object : JPanel() {
fun headerPanel(label: JLabel, component: JComponent?) = object : JPanel() {
init {
- background = UIUtil.getControlColor()
+ background = UIUtil.getLabelBackground()
layout = BorderLayout()
border = BorderFactory.createCompoundBorder(SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM), EmptyBorder(0, 5, 0, 5))
preferredSize = Dimension(preferredSize.width, 25)
diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java
index 903c48e9c581..68b2f99d8b6a 100644
--- a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java
+++ b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java
@@ -43,6 +43,8 @@ import com.jetbrains.python.PyBundle;
import com.jetbrains.python.codeInsight.typing.PyTypeShed;
import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil;
import com.jetbrains.python.packaging.PyPackageManager;
+import com.jetbrains.python.packaging.management.PythonPackageManagerExt;
+import com.jetbrains.python.packaging.management.PythonPackageManager;
import com.jetbrains.python.psi.PyUtil;
import com.jetbrains.python.remote.UnsupportedPythonSdkTypeException;
import com.jetbrains.python.sdk.skeletons.PySkeletonRefresher;
@@ -179,6 +181,10 @@ public class PythonSdkUpdater implements StartupActivity.Background {
indicator.setText(PyBundle.message("python.sdk.scanning.installed.packages"));
indicator.setText2("");
PyPackageManager.getInstance(sdk).refreshAndGetPackages(true);
+ PythonPackageManager manager = PythonPackageManager.Companion.forSdk(myProject, mySdk);
+ if (manager != null) {
+ PythonPackageManagerExt.launchReload(manager);
+ }
}
catch (ExecutionException e) {
if (LOG.isDebugEnabled()) {
diff --git a/python/src/com/jetbrains/python/sdk/flavors/conda/PyCondaEnv.kt b/python/src/com/jetbrains/python/sdk/flavors/conda/PyCondaEnv.kt
index bc769d55ef03..abf63c4d903e 100644
--- a/python/src/com/jetbrains/python/sdk/flavors/conda/PyCondaEnv.kt
+++ b/python/src/com/jetbrains/python/sdk/flavors/conda/PyCondaEnv.kt
@@ -87,6 +87,24 @@ suspend fun createEnv(command: PyCondaCommand, newCondaEnvInfo: NewCondaEnvRequ
}
}
+ /**
+ * Add conda prefix to [targetedCommandLineBuilder] without specifying the 'run' command
+ */
+ fun addCondaEnvironmentToTargetBuilder(targetedCommandLineBuilder: TargetedCommandLineBuilder) {
+ targetedCommandLineBuilder.apply {
+ when (val identity = this@PyCondaEnv.envIdentity) {
+ is PyCondaEnvIdentity.UnnamedEnv -> {
+ addParameter("-p")
+ addParameter(identity.envPath) // TODO: Escape. Shouldn't target have something like "addEscaped"?
+ }
+ is PyCondaEnvIdentity.NamedEnv -> {
+ addParameter("-n")
+ addParameter(identity.envName)
+ }
+ }
+ }
+ }
+
override fun toString(): String = "$envIdentity@$fullCondaPathOnTarget"
}