mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
2bf0bfbd11
commit
1b87f861c6
@@ -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
|
||||
}
|
||||
+91
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -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()
|
||||
|
||||
+109
-49
@@ -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")
|
||||
}
|
||||
|
||||
+108
-260
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user