[python]: new API for package management and conda support (PY-49324)

- new entry point to python package management: PythonPackageManager
- new internal package representation entities
- target-based process running to support remote interpreters
- initial support for the new api on Python Packages toolwindow
- initial support for conda on the toolwindow
- new cache for PyPI packages, caches for python/conda packages
- Python Packages toolwindow and service refactoring
- coroutine-based PyPI cache update on startup

GitOrigin-RevId: 167c32229eb669c5348ba930b35008bee9e51914
This commit is contained in:
Aleksei Kniazev
2022-10-18 00:37:48 +00:00
committed by intellij-monorepo-bot
parent 2bf0bfbd11
commit 1b87f861c6
36 changed files with 1420 additions and 387 deletions
@@ -110,6 +110,7 @@
<orderEntry type="library" name="caffeine" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
<orderEntry type="module" module-name="intellij.platform.ml" />
<orderEntry type="module" module-name="intellij.platform.extensions" />
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
<orderEntry type="library" name="jackson-dataformat-yaml" level="project" />
@@ -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=<html><head></head><body><p class="empty_description">Could not parse the response for the package {0} from repository {1}</p></body></html>
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=<html><head></head><body><p class="empty_description">Could not read package information</p></body></html>
python.packages.request.failed=<html><head></head><body><p class="empty_description">Request failed.</p></body></html>
# 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=<html><head></head><body><p class="empty_description">Request failed.</p></body></html>
python.toolwindow.packages.no.description.placeholder=<html><head></head><body><p class="empty_description">Package author did not provide a description.</p></body></html>
python.toolwindow.packages.remote.interpreter.placeholder=<html><head></head><body><p class="empty_description">Local interpreter is required to view package documentation.</p></body></html>
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
@@ -483,6 +483,8 @@
<notificationGroup id="ConfiguredPythonInterpreter" displayType="BALLOON" isLogByDefault="true" bundle="messages.PyBundle"
key="sdk.has.been.configured.notification.name"/>
<notificationGroup id="Packaging" displayType="BALLOON" bundle="messages.PyBundle" key="notification.group.packaging"/>
<notificationGroup id="PythonPackages" displayType="TOOL_WINDOW" toolWindowId="Python Packages"
bundle="messages.PyBundle" key="notification.group.packaging.toolwindow"/>
<notificationGroup id="pyproject.toml Watcher" displayType="STICKY_BALLOON" isLogByDefault="true" bundle="messages.PyBundle"
key="python.sdk.poetry.pip.file.watcher"/>
@@ -498,6 +500,8 @@
<!-- Code vision -->
<vcs.codeVisionLanguageContext language="Python" implementationClass="com.jetbrains.python.vcs.PyVcsContextProvider"/>
<backgroundPostStartupActivity implementation="com.jetbrains.python.packaging.management.PythonPackagesUpdater"/>
</extensions>
<extensionPoints>
@@ -590,6 +594,10 @@
<extensionPoint qualifiedName="Pythonid.connectionCredentialsToTargetConfigurationConverter"
interface="com.jetbrains.python.run.target.ConnectionCredentialsToTargetConfigurationConverter"
dynamic="true"/>
<extensionPoint qualifiedName="Pythonid.PythonPackagingToolwindowActionProvider"
interface="com.jetbrains.python.packaging.toolwindow.PythonPackagingToolwindowActionProvider"
dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="Pythonid">
@@ -685,6 +693,9 @@
<runConfigurationEditorExtension implementation="com.jetbrains.python.run.PyRunConfigurationTargetOptions"/>
<remoteSdkValidator implementation="com.jetbrains.python.target.PyTargetSdkValidator"/>
<PythonPackagingToolwindowActionProvider implementation="com.jetbrains.python.packaging.pip.PipPackagingToolwindowActionProvider"/>
<PythonPackagingToolwindowActionProvider implementation="com.jetbrains.python.packaging.conda.CondaPackagingToolwindowActionProvider"/>
</extensions>
<actions>
@@ -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 }
@@ -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<String>
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<Array<String>>::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<Array<String>>::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)
}
}
@@ -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;
@@ -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<K> {
val packages: List<String>
operator fun contains(key: K): Boolean
fun isEmpty(): Boolean
}
@@ -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<PyPackageRepository> {
private var cache: Map<PyPackageRepository, List<String>> = emptyMap()
val repositories: List<PyPackageRepository>
get() = cache.keys.toList()
override val packages: List<String>
get() = cache.values.asSequence().flatten().toList()
private val userAgent: String
get() = "${ApplicationNamesInfo.getInstance().productName}/${ApplicationInfo.getInstance().fullVersion}"
suspend fun refresh() {
val service = service<PyPackageRepositories>()
withContext(Dispatchers.IO) {
val newCache = mutableMapOf<PyPackageRepository, List<String>>()
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<String> {
return withContext(Dispatchers.IO) {
val packages = mutableListOf<String>()
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<String>? = cache[key]
override fun isEmpty(): Boolean = cache.isEmpty()
override fun contains(key: PyPackageRepository): Boolean = key in cache
}
@@ -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<String>
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<String> = 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<String> = 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<String> {
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<String> = 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
@@ -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<String, PythonPackageManager>()
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<String> = 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)
}
}
@@ -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<String> {
private var cache: Map<String, List<String>> = emptyMap()
override val packages: List<String>
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<String>? = cache[name]
override fun contains(key: String): Boolean = key in cache
}
@@ -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<CondaPackage> = 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<CondaPackage> {
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<String>, @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
}
}
}
@@ -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<PythonPackageInstallAction>? {
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
}
}
@@ -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<PyPackageRepository>
get() = listOf(CondaPackageRepository) + super.repositories
override fun allPackages(): List<String> = CondaPackageCache.packages
override fun packagesFromRepository(repository: PyPackageRepository): List<String> {
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)
}
}
@@ -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<String> {
return listOf("$name${if (version != null) "=$version" else ""}")
}
}
class CondaPackageDetails(override val name: String,
override val availableVersions: List<String> = 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)
}
}
@@ -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<PythonPackage>
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)
}
}
@@ -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<String>, @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<String> = 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<Pair<PyPackageRepository, List<String>>> {
return repositories.asSequence().map { it to packagesFromRepository(it) }
}
@@ -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())
}
}
@@ -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<PyPackageRepository>
abstract fun allPackages(): List<String>
abstract fun packagesFromRepository(repository: PyPackageRepository): List<String>
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
}
@@ -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()
}
}
}
@@ -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<PyPackageRepository>
get() = listOf(PyPIPackageRepository) + PythonSimpleRepositoryCache.repositories
private val gson = Gson()
private val packageDetailsCache = Caffeine.newBuilder()
.maximumSize(10)
.expireAfterWrite(Duration.ofHours(1))
.build<PythonPackageSpecification, PythonPackageDetails> {
// todo[akniazev] make it possible to show info from several repos
val repositoryUrl = it.repository?.repositoryUrl ?: PyPIPackageRepository.repositoryUrl!!
val 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<String>? {
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<PyPackageRepositories>()
if (service.repositories.isNotEmpty() && PythonSimpleRepositoryCache.isEmpty()) {
PythonSimpleRepositoryCache.refresh()
}
}
override suspend fun refreshCashes() {
PypiPackageCache.refresh()
PythonSimpleRepositoryCache.refresh()
}
override fun allPackages(): List<String> {
// todo[akniazev] check if it is even needed
return packagesByRepository().flatMap { it.second }.distinct().toList()
}
override fun packagesFromRepository(repository: PyPackageRepository): List<String> {
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]
}
}
@@ -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<PythonPackageInstallAction>? {
if (packageManager is PipPythonPackageManager && details is PythonSimplePackageDetails)
return listOf(SimplePythonPackageInstallAction(PyBundle.message("python.packaging.button.install.package"), packageManager.project))
return null
}
}
@@ -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<PythonPackage> = 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)
}
}
}
@@ -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)
@@ -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<String> {
private val gson: Gson = GsonBuilder()
.registerTypeAdapter(object : TypeToken<TreeSet<String>>() {}.type,
object : InstanceCreator<TreeSet<String>> {
override fun createInstance(type: Type?): TreeSet<String> {
return TreeSet<String>(RANKING_AWARE_PACKAGE_NAME_COMPARATOR)
}
})
.create()
override val packages: List<String>
get() = cache.toList()
private var cache: TreeSet<String> = 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<TreeSet<String>>() {}.type
val newCache = Files.newBufferedReader(filePath, StandardCharsets.UTF_8).use { reader ->
val newCache: TreeSet<String> = 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()
}
@@ -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)
}
}
@@ -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)
@@ -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<PyPackagingToolWindowService>()
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)
@@ -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<PyPackagingTableGroup<DisplayablePackage>> = 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<String, JPanel> = 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<InstalledPackage>) {
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<InstalledPackage>) {
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()
@@ -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<PyPackagingToolWindowService>()
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<String>(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<String, Boolean>? {
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<String, Boolean>? {
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<PyPackagingToolWindowService>()
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<InstalledPackage>, repoData: List<PyPackagesViewData>) {
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<InstalledPackage>) {
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")
}
@@ -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<InstalledPackage> = 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<PyInvalidRepositoryViewData>
get() = service<PyPackageRepositories>().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("<p>${message("python.toolwindow.packages.no.documentation")}</p>"),
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<PyPackageRepositories>().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<PyPackageRepositories>()
.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<PyPackageRepositories>().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<InstalledPackage>) -> 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<InstalledPackage>? = null
var newPackages: List<InstalledPackage>? = 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<RepoPackage>,
private fun filterPackagesForRepo(packageNames: List<String>,
query: String,
url: String,
repository: PyPackageRepository,
skipItems: Int = 0): PyPackagesViewData {
val comparator = createComparator(query, url)
val repository = service<PyPackageRepositories>().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<String> = 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><head></head><body><p>$html</p></body></html>"
fun wrapHtml(html: String): String = "<html><head></head><body><p>$html</p></body></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<PyPackageRepositories>()
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<ExecutionException>?) {
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<RepoPackage> {
val nameComparator = Comparator<RepoPackage> { o1, o2 ->
val name1 = o1.name.toLowerCase()
val name2 = o2.name.toLowerCase()
val queryLowerCase = query.toLowerCase()
private fun createNameComparator(query: String, url: String): Comparator<String> {
val nameComparator = Comparator<String> { 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
@@ -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<PythonPackageInstallAction>?
companion object {
val EP_NAME = ExtensionPointName.create<PythonPackagingToolwindowActionProvider>("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<PyPackagingToolWindowService>().installPackage(specification)
}
}
@@ -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<String>)
open class PyPackagesViewData(@NlsSafe val repository: PyPackageRepository, val packages: List<DisplayablePackage>, val exactMatch: Int = -1, val moreItems: Int = 0)
class PyInvalidRepositoryViewData(repository: PyPackageRepository) : PyPackagesViewData(repository, emptyList())
@@ -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<T : DisplayablePackage>(model: ListTableModel<T>, service: PyPackagingToolWindowService, tablesView: PyPackagingTablesView) : JBTable(model) {
internal class PyPackagesTable<T : DisplayablePackage>(project: Project,
model: ListTableModel<T>,
tablesView: PyPackagingTablesView,
controller: PyPackagingToolWindowPanel) : JBTable(model) {
private var lastSelectedRow = -1
init {
val service = project.service<PyPackagingToolWindowService>()
setShowGrid(false)
setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
val column = columnModel.getColumn(1)
@@ -42,7 +48,7 @@ internal class PyPackagesTable<T : DisplayablePackage>(model: ListTableModel<T>,
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)
@@ -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()) {
@@ -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"
}