extract common code for listing installed/outdated packages for poetry; #PY-78077 Fixed

GitOrigin-RevId: ba1410bab2aeb4b69e6a9b134fd81331b7de3b06
This commit is contained in:
Aleksandr Sorotskii
2025-01-10 14:56:08 +01:00
committed by intellij-monorepo-bot
parent b21ed9e4e9
commit 3f017485ef
3 changed files with 95 additions and 75 deletions

View File

@@ -16,8 +16,12 @@ import com.intellij.openapi.util.io.FileUtil
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.util.SystemProperties
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackage
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.pathValidation.PlatformAndRoot
@@ -198,7 +202,7 @@ fun parsePoetryShow(input: String): List<PythonPackage> {
val result = mutableListOf<PythonPackage>()
input.split("\n").forEach { line ->
if (line.isNotBlank()) {
val packageInfo = line.trim().split(" ").map { it.trim() }.filter { it.isNotBlank() && it != "(!)"}
val packageInfo = line.trim().split(" ").map { it.trim() }.filter { it.isNotBlank() && it != "(!)" }
result.add(PythonPackage(packageInfo[0], packageInfo[1], false))
}
}
@@ -206,6 +210,70 @@ fun parsePoetryShow(input: String): List<PythonPackage> {
return result
}
@Internal
suspend fun poetryShowOutdated(sdk: Sdk): Result<Map<String, PythonOutdatedPackage>> {
val output = runPoetryWithSdk(sdk, "show", "--outdated").getOrElse {
return Result.failure(it)
}
return parsePoetryShowOutdated(output).let { Result.success(it) }
}
@Internal
suspend fun poetryListPackages(sdk: Sdk): Result<Pair<List<PyPackage>, List<PyRequirement>>> {
// Just in case there were any changes to pyproject.toml
if (runPoetryWithSdk(sdk, "lock", "--check").isFailure) {
runPoetryWithSdk(sdk, "lock", "--no-update")
}
val output = runPoetryWithSdk(sdk, "install", "--dry-run", "--no-root").getOrElse {
return Result.failure(it)
}
return parsePoetryInstallDryRun(output).let {
Result.success(it)
}
}
@Internal
fun parsePoetryInstallDryRun(input: String): Pair<List<PyPackage>, List<PyRequirement>> {
val installedLines = listOf("Already installed", "Skipping", "Updating")
fun getNameAndVersion(line: String): Triple<String, String, String> {
return line.split(" ").let {
val installedVersion = it[5].replace(Regex("[():]"), "")
val requiredVersion = when {
it.size > 7 && it[6] == "->" -> it[7].replace(Regex("[():]"), "")
else -> installedVersion
}
Triple(it[4], installedVersion, requiredVersion)
}
}
fun getVersion(version: String): String {
return if (Regex("^[0-9]").containsMatchIn(version)) "==$version" else version
}
val pyPackages = mutableListOf<PyPackage>()
val pyRequirements = mutableListOf<PyRequirement>()
input
.lineSequence()
.filter { listOf(")", "Already installed").any { lastWords -> it.endsWith(lastWords) } }
.forEach { line ->
getNameAndVersion(line).also {
if (installedLines.any { installedLine -> line.contains(installedLine) }) {
pyPackages.add(PyPackage(it.first, it.second, null, emptyList()))
PyRequirementParser.fromLine(it.first + getVersion(it.third))?.let { pyRequirement -> pyRequirements.add(pyRequirement) }
}
else if (line.contains("Installing")) {
PyRequirementParser.fromLine(it.first + getVersion(it.third))?.let { pyRequirement -> pyRequirements.add(pyRequirement) }
}
}
}
return Pair(pyPackages.distinct().toList(), pyRequirements.distinct().toList())
}
/**
* Configures the Poetry environment for the specified module path with the given arguments.
* Runs command: GeneralCommandLine("poetry config [args]").withWorkingDirectory([modulePath])

View File

@@ -27,29 +27,23 @@ class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr
override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<String> = poetryUninstallPackage(sdk, pkg.name)
override suspend fun reloadPackagesCommand(): Result<List<PythonPackage>> {
return poetryShowPackages(sdk)
}
val (installed, _) = poetryListPackages(sdk).getOrElse {
return Result.failure(it)
}
override suspend fun reloadPackages(): Result<List<PythonPackage>> {
updateOutdatedPackages()
return super.reloadPackages()
outdatedPackages = poetryShowOutdated(sdk).getOrElse {
emptyMap()
}
val packages = installed.map {
PythonPackage(it.name, it.version, false)
}
return Result.success(packages)
}
internal fun getOutdatedPackages(): Map<String, PythonOutdatedPackage> = outdatedPackages
/**
* Updates the list of outdated packages by running the Poetry command
* `poetry show --outdated`, parsing its output, and storing the results.
*/
private suspend fun updateOutdatedPackages() {
val outputOutdatedPackages = runPoetryWithSdk(sdk, "show", "--outdated").getOrElse {
outdatedPackages = emptyMap()
return
}
outdatedPackages = parsePoetryShowOutdated(outputOutdatedPackages)
}
private fun PythonPackageSpecification.getVersionForPoetry(): String = if (versionSpecs == null) name else "$name@$versionSpecs"
}

View File

@@ -17,8 +17,6 @@ import com.jetbrains.python.sdk.associatedModuleDir
class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) {
private val installedLines = listOf("Already installed", "Skipping", "Updating")
@Volatile
private var packages: List<PyPackage>? = null
@@ -37,13 +35,13 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) {
override fun install(requirements: List<PyRequirement>?, extraArgs: List<String>) {
val args = if (requirements.isNullOrEmpty()) {
listOfNotNull(listOf("install"),
extraArgs)
extraArgs)
.flatten()
}
else {
listOfNotNull(listOf("add"),
requirements.map { it.name },
extraArgs)
requirements.map { it.name },
extraArgs)
.flatten()
}
@@ -87,29 +85,27 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) {
override fun getPackages() = packages
fun getOutdatedPackages() = outdatedPackages
override fun refreshAndGetPackages(alwaysRefresh: Boolean): List<PyPackage> {
if (alwaysRefresh || packages == null) {
packages = null
val outputInstallDryRun = runBlockingCancellable { runPoetryWithSdk(sdk, "install", "--dry-run", "--no-root") }.getOrElse {
val allPackages = runBlockingCancellable {
poetryListPackages(sdk)
}.getOrElse {
packages = emptyList()
return packages ?: emptyList()
return emptyList()
}
val allPackage = parsePoetryInstallDryRun(outputInstallDryRun)
packages = allPackage.first
requirements = allPackage.second
packages = allPackages.first
requirements = allPackages.second
val outputOutdatedPackages = runBlockingCancellable { runPoetryWithSdk(sdk, "show", "--outdated") }.getOrElse {
outdatedPackages = emptyMap()
runBlockingCancellable {
outdatedPackages = poetryShowOutdated(sdk).getOrElse {
emptyMap()
}
}
if (outputOutdatedPackages is String) {
outdatedPackages = parsePoetryShowOutdated(outputOutdatedPackages)
}
ApplicationManager.getApplication().messageBus.syncPublisher(PACKAGE_MANAGER_TOPIC).packagesRefreshed(sdk)
}
return packages ?: emptyList()
}
@@ -130,42 +126,4 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) {
// TODO: Parse the dependency information from `pipenv graph`
return emptySet()
}
private fun getVersion(version: String): String {
return if (Regex("^[0-9]").containsMatchIn(version)) "==$version" else version
}
/**
* Parses the output of `poetry install --dry-run ` into a list of packages.
*/
private fun parsePoetryInstallDryRun(input: String): Pair<List<PyPackage>, List<PyRequirement>> {
fun getNameAndVersion(line: String): Triple<String, String, String> {
return line.split(" ").let {
val installedVersion = it[5].replace(Regex("[():]"), "")
val requiredVersion = when {
it.size > 7 && it[6] == "->" -> it[7].replace(Regex("[():]"), "")
else -> installedVersion
}
Triple(it[4], installedVersion, requiredVersion)
}
}
val pyPackages = mutableListOf<PyPackage>()
val pyRequirements = mutableListOf<PyRequirement>()
input
.lineSequence()
.filter { listOf(")", "Already installed").any { lastWords -> it.endsWith(lastWords) } }
.forEach { line ->
getNameAndVersion(line).also {
if (installedLines.any { installedLine -> line.contains(installedLine) }) {
pyPackages.add(PyPackage(it.first, it.second, null, emptyList()))
this.parseRequirement(it.first + getVersion(it.third))?.let { pyRequirement -> pyRequirements.add(pyRequirement) }
}
else if (line.contains("Installing")) {
this.parseRequirement(it.first + getVersion(it.third))?.let { pyRequirement -> pyRequirements.add(pyRequirement) }
}
}
}
return Pair(pyPackages.distinct().toList(), pyRequirements.distinct().toList())
}
}