PY-59838 Refactor Poetry package management

Associate poetry files ("poetry.lock", "pyproject.toml") with the Python Packages toolwindow.
Separate UI error handling from package management logic.
Add tests to check the installation and removal of packages using poetry and "pyproject.toml" modification.

Merge-request: IJ-MR-146002
Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com>

(cherry picked from commit 2ab0816f10c970f738d6d931dc123481030cad38)


Merge-request: IJ-MR-148435
Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com>

GitOrigin-RevId: be957c5343b73264c78134f156ad0e4034b912f9
This commit is contained in:
Egor Eliseev
2024-11-11 15:50:51 +00:00
committed by intellij-monorepo-bot
parent 865f1aa38e
commit dbee69ed0b
25 changed files with 373 additions and 193 deletions

View File

@@ -14,8 +14,8 @@ import kotlinx.coroutines.withContext
class PyV3EmptyProjectSettings(var generateWelcomeScript: Boolean = false) : PyV3ProjectTypeSpecificSettings {
override suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk) {
if (!generateWelcomeScript) return
override suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk): Result<Boolean> {
if (!generateWelcomeScript) return Result.success(false)
val file = writeAction {
PyWelcome.prepareFile(module.project, baseDir)
}
@@ -24,6 +24,8 @@ class PyV3EmptyProjectSettings(var generateWelcomeScript: Boolean = false) : PyV
file.navigate(true)
}
}
return Result.success(true)
}
override fun toString(): String {

View File

@@ -851,6 +851,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<pySdkProvider implementation="com.jetbrains.python.sdk.poetry.PoetrySdkProvider"/>
<packageManagerProvider implementation="com.jetbrains.python.sdk.poetry.PyPoetryPackageManagerProvider"/>
<pythonPackageManagerProvider implementation="com.jetbrains.python.sdk.poetry.PoetryPackageManagerProvider"/>
<pythonPackageManagerProvider implementation="com.jetbrains.python.packaging.pip.PipPackageManagerProvider" order="last"/>

View File

@@ -56,7 +56,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
// Either base settings (which create venv) might generate some or type specific settings (like Django) may.
// So we expand it right after SDK generation, but if there are no files yet, we do it again after project generation
ensureProjectViewExpanded(project)
typeSpecificSettings.generateProject(module, baseDir, sdk)
typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure { errorSink.emit(it.localizedMessage) }
ensureProjectViewExpanded(project)
}
}

View File

@@ -20,5 +20,5 @@ fun interface PyV3ProjectTypeSpecificSettings {
* Generate project-specific things in [baseDir].
* You might need to [installPackages] on [sdk]
*/
suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk)
suspend fun generateProject(module: Module, baseDir: VirtualFile, sdk: Sdk): Result<Boolean>
}

View File

@@ -5,20 +5,19 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.common.PythonSimplePackageSpecification
import com.jetbrains.python.packaging.management.PythonPackageManager
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
/**
* Install [packages] to [sdk]
*/
suspend fun installPackages(project: Project, sdk: Sdk, vararg packages: String) {
suspend fun installPackages(project: Project, sdk: Sdk, vararg packages: String): Result<Boolean> {
val packageManager = PythonPackageManager.forSdk(project, sdk)
supervisorScope { // Not install other packages if one failed
return supervisorScope { // Not install other packages if one failed
for (packageName in packages) {
launch {
packageManager.installPackage(PythonSimplePackageSpecification(packageName, null, null), emptyList()).getOrThrow()
}
val packageSpecification = PythonSimplePackageSpecification(packageName, null, null)
packageManager.installPackage(packageSpecification, emptyList<String>()).onFailure { return@supervisorScope Result.failure(it) }
}
return@supervisorScope Result.success(true)
}
}

View File

@@ -18,6 +18,7 @@ import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.inspections.PyPackageRequirementsInspection.InstallPackageQuickFix
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.management.PythonPackageManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View File

@@ -143,7 +143,11 @@ class PythonPackageManagementServiceBridge(project: Project,sdk: Sdk) : PyPackag
manager
.installedPackages
.filter { it.name.lowercase() in namesToDelete }
.forEach { manager.uninstallPackage(it) }
.forEach {
runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
manager.uninstallPackage(it)
}
}
listener.operationFinished(namesToDelete.first(), null)
}

View File

@@ -9,9 +9,26 @@ import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import org.jetbrains.annotations.Nls
open class PythonPackage(val name: String, val version: String, val isEditableMode: Boolean) {
companion object {
private const val HASH_MULTIPLIER = 31
}
override fun toString(): String {
return "PythonPackage(name='$name', version='$version')"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PythonPackage) return false
return name == other.name && version == other.version && isEditableMode == other.isEditableMode
}
override fun hashCode(): Int {
var result = name.hashCode()
result = HASH_MULTIPLIER * result + version.hashCode()
result = HASH_MULTIPLIER * result + isEditableMode.hashCode()
return result
}
}
interface PythonPackageDetails {

View File

@@ -1,12 +1,12 @@
// 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.ExecutionException
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessOutput
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.project.Project
import com.intellij.openapi.projectRoots.Sdk
@@ -17,7 +17,6 @@ import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.pip.PipBasedPackageManager
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
import com.jetbrains.python.sdk.getOrCreateAdditionalData
@@ -29,64 +28,56 @@ import org.jetbrains.annotations.Nls
@ApiStatus.Experimental
class CondaPackageManager(project: Project, sdk: Sdk) : PipBasedPackageManager(project, sdk) {
@Volatile
override var installedPackages: List<CondaPackage> = emptyList()
private set
override val repositoryManager: CondaRepositoryManger = CondaRepositoryManger(project, sdk)
override suspend fun installPackage(specification: PythonPackageSpecification, options: List<String>): Result<List<PythonPackage>> {
return if (specification is CondaPackageSpecification) {
runPackagingOperationOrShowErrorDialog(sdk, message("python.new.project.install.failed.title", specification.name), specification.name) {
runConda("install", specification.buildInstallationString() + "-y" + options, message("conda.packaging.install.progress", specification.name))
refreshPaths()
reloadPackages()
override suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List<String>): Result<String> =
if (specification is CondaPackageSpecification) {
try {
Result.success(runConda("install", specification.buildInstallationString() + "-y" + options, message("conda.packaging.install.progress", specification.name)))
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
}
else return super.installPackage(specification, emptyList<String>())
}
else {
super.installPackageCommand(specification, options)
}
override suspend fun uninstallPackage(pkg: PythonPackage): Result<List<PythonPackage>> {
return if (pkg is CondaPackage && !pkg.installedWithPip) {
runPackagingOperationOrShowErrorDialog(sdk, message("python.packaging.operation.failed.title")) {
runConda("uninstall", listOf(pkg.name, "-y"), message("conda.packaging.uninstall.progress", pkg.name))
refreshPaths()
reloadPackages()
override suspend fun updatePackageCommand(specification: PythonPackageSpecification): Result<String> =
if (specification is CondaPackageSpecification) {
try {
Result.success(runConda("update", listOf(specification.name, "-y"), message("conda.packaging.update.progress", specification.name)))
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
}
else super.uninstallPackage(pkg)
}
else {
super.updatePackageCommand(specification)
}
override suspend fun updatePackage(specification: PythonPackageSpecification): Result<List<PythonPackage>> {
return if (specification is CondaPackageSpecification) {
runPackagingOperationOrShowErrorDialog(sdk, message("python.packaging.notification.update.failed", specification.name), specification.name) {
runConda("update", listOf(specification.name, "-y"), message("conda.packaging.update.progress", specification.name))
refreshPaths()
reloadPackages()
override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<String> =
if (pkg is CondaPackage && !pkg.installedWithPip) {
try {
Result.success(runConda("uninstall", listOf(pkg.name, "-y"), message("conda.packaging.uninstall.progress", pkg.name)))
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
}
else super.updatePackage(specification)
}
override suspend fun reloadPackages(): Result<List<PythonPackage>> {
return withContext(Dispatchers.IO) {
val result = runPackagingOperationOrShowErrorDialog(sdk, message("python.packaging.operation.failed.title")) {
val output = runConda("list", emptyList(), message("conda.packaging.list.progress"))
Result.success(parseCondaPackageList(output))
}
if (result.isFailure) return@withContext result
installedPackages = result.getOrThrow()
ApplicationManager.getApplication()
.messageBus
.syncPublisher(PACKAGE_MANAGEMENT_TOPIC)
.packagesChanged(sdk)
result
else {
super.uninstallPackageCommand(pkg)
}
override suspend fun reloadPackagesCommand(): Result<List<PythonPackage>> =
try {
val output =runConda("list", emptyList(), message("conda.packaging.list.progress"))
Result.success(parseCondaPackageList(output))
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
}
private fun parseCondaPackageList(text: String): List<CondaPackage> {
return text.lineSequence()

View File

@@ -17,17 +17,36 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
abstract val installedPackages: List<PythonPackage>
abstract var installedPackages: List<PythonPackage>
abstract val repositoryManager: PythonRepositoryManager
abstract suspend fun installPackage(specification: PythonPackageSpecification, options: List<String>): Result<List<PythonPackage>>
suspend fun installPackage(specification: PythonPackageSpecification, options: List<String>): Result<List<PythonPackage>> {
installPackageCommand(specification, options).onFailure { return Result.failure(it) }
refreshPaths()
return reloadPackages()
}
abstract suspend fun updatePackage(specification: PythonPackageSpecification): Result<List<PythonPackage>>
abstract suspend fun uninstallPackage(pkg: PythonPackage): Result<List<PythonPackage>>
suspend fun updatePackage(specification: PythonPackageSpecification): Result<List<PythonPackage>> {
updatePackageCommand(specification).onFailure { return Result.failure(it) }
refreshPaths()
return reloadPackages()
}
suspend fun uninstallPackage(pkg: PythonPackage): Result<List<PythonPackage>> {
uninstallPackageCommand(pkg).onFailure { return Result.failure(it) }
refreshPaths()
return reloadPackages()
}
abstract suspend fun reloadPackages(): Result<List<PythonPackage>>
protected abstract suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List<String>): Result<String>
protected abstract suspend fun updatePackageCommand(specification: PythonPackageSpecification): Result<String>
protected abstract suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<String>
protected abstract suspend fun reloadPackagesCommand(): Result<List<PythonPackage>>
internal suspend fun refreshPaths() {
writeAction {
VfsUtil.markDirtyAndRefresh(true, true, true, *sdk.rootProvider.getFiles(OrderRootType.CLASSES))
@@ -35,7 +54,6 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
}
}
companion object {
fun forSdk(project: Project, sdk: Sdk): PythonPackageManager {
return project.service<PackageManagerHolder>().forSdk(project, sdk)

View File

@@ -18,11 +18,13 @@ import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.util.net.HttpConfigurable
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.normalizePackageName
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.run.buildTargetedCommandLine
@@ -36,7 +38,9 @@ import kotlin.math.min
fun PythonPackageManager.launchReload() {
(ApplicationManager.getApplication() as ComponentManagerEx).getCoroutineScope().launch {
reloadPackages()
runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
reloadPackages()
}
}
}

View File

@@ -1,39 +1,73 @@
// 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.execution.ExecutionException
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.PyPackageManager
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.runPackagingTool
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
abstract class PipBasedPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
override suspend fun installPackage(specification: PythonPackageSpecification, options: List<String>): Result<List<PythonPackage>> {
return runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.new.project.install.failed.title", specification.name), specification.name) {
runPackagingTool("install", specification.buildInstallationString() + options, PyBundle.message("python.packaging.install.progress", specification.name))
refreshPaths()
reloadPackages()
}
}
@Volatile
override var installedPackages: List<PythonPackage> = emptyList()
override suspend fun updatePackage(specification: PythonPackageSpecification): Result<List<PythonPackage>> {
return runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.notification.update.failed", specification.name), specification.name) {
runPackagingTool("install", listOf("--upgrade") + specification.buildInstallationString(), PyBundle.message("python.packaging.update.progress", specification.name))
refreshPaths()
reloadPackages()
override suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List<String>): Result<String> =
try {
Result.success(runPackagingTool("install", specification.buildInstallationString() + options, PyBundle.message("python.packaging.install.progress", specification.name)))
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
}
override suspend fun uninstallPackage(pkg: PythonPackage): Result<List<PythonPackage>> {
return runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
runPackagingTool("uninstall", listOf(pkg.name), PyBundle.message("python.packaging.uninstall.progress", pkg.name))
refreshPaths()
reloadPackages()
override suspend fun updatePackageCommand(specification: PythonPackageSpecification): Result<String> =
try {
Result.success(runPackagingTool("install", listOf("--upgrade") + specification.buildInstallationString(), PyBundle.message("python.packaging.update.progress", specification.name)))
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<String> =
try {
Result.success(runPackagingTool("uninstall", listOf(pkg.name), PyBundle.message("python.packaging.uninstall.progress", pkg.name)))
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
override suspend fun reloadPackagesCommand(): Result<List<PythonPackage>> =
try {
val output = runPackagingTool("list", emptyList(), PyBundle.message("python.packaging.list.progress"))
val packages = output.lineSequence()
.filter { it.isNotBlank() }
.map {
val line = it.split("\t")
PythonPackage(line[0], line[1], isEditableMode = false)
}
.sortedWith(compareBy(PythonPackage::name))
.toList()
Result.success(packages)
}
catch (ex: ExecutionException) {
Result.failure(ex)
}
override suspend fun reloadPackages(): Result<List<PythonPackage>> {
val packages = reloadPackagesCommand().onFailure { return Result.failure(it) }.getOrThrow()
installedPackages = packages
ApplicationManager.getApplication().messageBus.apply {
syncPublisher(PACKAGE_MANAGEMENT_TOPIC).packagesChanged(sdk)
syncPublisher(PyPackageManager.PACKAGE_MANAGER_TOPIC).packagesRefreshed(sdk)
}
return Result.success(packages)
}
}

View File

@@ -1,51 +1,11 @@
// Copyright 2000-2023 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.intellij.platform.backend.observation.trackActivity
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.management.runPackagingTool
import com.jetbrains.python.sdk.headless.PythonActivityKey
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
class PipPythonPackageManager(project: Project, sdk: Sdk) : PipBasedPackageManager(project, sdk) {
@Volatile
override var installedPackages: List<PythonPackage> = emptyList()
private set
override val repositoryManager: PipRepositoryManager = PipRepositoryManager(project, sdk)
override suspend fun reloadPackages(): Result<List<PythonPackage>> = project.trackActivity(PythonActivityKey) {
val result = runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
val output = runPackagingTool("list", emptyList(), PyBundle.message("python.packaging.list.progress"))
val packages = output.lineSequence()
.filter { it.isNotBlank() }
.map {
val line = it.split("\t")
PythonPackage(line[0], line[1], isEditableMode = false)
}
.sortedWith(compareBy(PythonPackage::name))
.toList()
Result.success(packages)
}
if (result.isFailure) return@trackActivity result
installedPackages = result.getOrThrow()
ApplicationManager.getApplication().messageBus.apply {
syncPublisher(PACKAGE_MANAGEMENT_TOPIC).packagesChanged(sdk)
syncPublisher(PyPackageManager.PACKAGE_MANAGER_TOPIC).packagesRefreshed(sdk)
}
return@trackActivity result
}
}

View File

@@ -20,12 +20,14 @@ import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.util.text.StringUtil
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportRawProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.normalizePackageName
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.packagesByRepository
@@ -106,18 +108,25 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
suspend fun installPackage(specification: PythonPackageSpecification, options: List<String> = emptyList()) {
PythonPackagesToolwindowStatisticsCollector.installPackageEvent.log(project)
val result = manager.installPackage(specification, options = options)
val result = runPackagingOperationOrShowErrorDialog(manager.sdk, message("python.new.project.install.failed.title", specification.name), specification.name) {
manager.installPackage(specification, options)
}
if (result.isSuccess) showPackagingNotification(message("python.packaging.notification.installed", specification.name))
}
suspend fun deletePackage(selectedPackage: InstalledPackage) {
PythonPackagesToolwindowStatisticsCollector.uninstallPackageEvent.log(project)
val result = manager.uninstallPackage(selectedPackage.instance)
val result = runPackagingOperationOrShowErrorDialog(manager.sdk, message("python.packaging.operation.failed.title")) {
manager.uninstallPackage(selectedPackage.instance)
}
if (result.isSuccess) showPackagingNotification(message("python.packaging.notification.deleted", selectedPackage.name))
}
suspend fun updatePackage(specification: PythonPackageSpecification) {
val result = manager.updatePackage(specification)
val result = runPackagingOperationOrShowErrorDialog(manager.sdk, message("python.packaging.notification.update.failed", specification.name), specification.name) {
manager.updatePackage(specification)
}
if (result.isSuccess) showPackagingNotification(message("python.packaging.notification.updated", specification.name, specification.versionSpecs))
}
@@ -138,7 +147,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
}
manager = PythonPackageManager.forSdk(project, currentSdk!!)
manager.repositoryManager.initCaches()
manager.reloadPackages()
runPackagingOperationOrShowErrorDialog(sdk, message("python.packaging.operation.failed.title")) {
manager.reloadPackages()
}
withContext(Dispatchers.Main) {
toolWindowPanel?.contentVisible = currentSdk != null
@@ -285,7 +296,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
serviceScope.launch(Dispatchers.IO) {
withBackgroundProgress(project, message("python.packaging.loading.packages.progress.text"), cancellable = false) {
reportRawProgress {
manager.reloadPackages()
runPackagingOperationOrShowErrorDialog(manager.sdk, message("python.packaging.operation.failed.title")) {
manager.reloadPackages()
}
refreshInstalledPackages()
manager.repositoryManager.refreshCashes()
}

View File

@@ -155,7 +155,10 @@ class InstallRequirementQuickFix(requirement: Requirement) : LocalQuickFix {
val name = requirement.displayName
project.service<PyPackagingToolWindowService>().serviceScope.launch(Dispatchers.IO) {
manager.installPackage(manager.repositoryManager.createSpecification(name, versionSpec) ?: return@launch, emptyList<String>())
val specification = manager.repositoryManager.createSpecification(name, versionSpec) ?: return@launch
runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.new.project.install.failed.title", specification.name), specification.name) {
manager.installPackage(specification, emptyList<String>())
}
DaemonCodeAnalyzer.getInstance(project).restart(file)
}
}
@@ -200,7 +203,9 @@ class InstallProjectAsEditableQuickfix : LocalQuickFix {
runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.pyproject.install.self.error"), null) {
manager.runPackagingTool("install", listOf("-e", "."), PyBundle.message("python.pyproject.install.self.as.editable.progress"))
manager.refreshPaths()
manager.reloadPackages()
runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.packaging.operation.failed.title")) {
manager.reloadPackages()
}
}
DaemonCodeAnalyzer.getInstance(project).restart(file)
}

View File

@@ -26,7 +26,7 @@ internal object Logger {
* @return A [Result] object containing the output of the command execution.
*/
@RequiresBackgroundThread
internal fun runCommandLine(commandLine: GeneralCommandLine): Result<ProcessOutput> {
internal fun runCommandLine(commandLine: GeneralCommandLine): Result<String> {
Logger.LOG.info("Running command: ${commandLine.commandLineString}")
val commandOutput = with(CapturingProcessHandler(commandLine)) {
runProcess()
@@ -39,7 +39,7 @@ internal fun runCommandLine(commandLine: GeneralCommandLine): Result<ProcessOutp
)
}
fun runCommand(executable: Path, projectPath: Path?, @NlsContexts.DialogMessage errorMessage: String, vararg args: String): String {
fun runCommand(executable: Path, projectPath: Path?, @NlsContexts.DialogMessage errorMessage: String, vararg args: String): Result<String> {
val command = listOf(executable.absolutePathString()) + args
val commandLine = GeneralCommandLine(command).withWorkingDirectory(projectPath)
val handler = CapturingProcessHandler(commandLine)
@@ -55,7 +55,7 @@ fun runCommand(executable: Path, projectPath: Path?, @NlsContexts.DialogMessage
}
}
return processOutput(result, executable.pathString, args.asList(), errorMessage).getOrThrow().stdout.trim()
return processOutput(result, executable.pathString, args.asList(), errorMessage)
}
/**
@@ -72,14 +72,14 @@ internal fun processOutput(
commandString: String,
args: List<String>,
@NlsContexts.DialogMessage errorMessage: String = "",
): Result<ProcessOutput> {
): Result<String> {
return with(output) {
when {
isCancelled ->
Result.failure(RunCanceledByUserException())
exitCode != 0 ->
Result.failure(PyExecutionException(errorMessage, commandString, args, stdout, stderr, exitCode, emptyList()))
else -> Result.success(output)
else -> Result.success(output.stdout.trim())
}
}
}

View File

@@ -49,7 +49,7 @@ internal class PackageInstallationFilesService {
* @return A [Result] object that represents the [ProcessOutput] of the installation command.
*/
@RequiresBackgroundThread
internal suspend fun installPackageWithPython(url: URL, pythonExecutable: String): Result<ProcessOutput> {
internal suspend fun installPackageWithPython(url: URL, pythonExecutable: String): Result<String> {
val installationFile = downloadFile(url).getOrThrow()
val command = GeneralCommandLine(pythonExecutable, installationFile.absolutePathString())
return runCommandLine(command)

View File

@@ -168,7 +168,7 @@ fun runPipEnv(projectPath: @SystemDependent String, vararg args: String): String
PyBundle.message("python.sdk.pipenv.execution.exception.no.pipenv.message"),
"pipenv", emptyList(), ProcessOutput())
@Suppress("DialogTitleCapitalization")
return runCommand(executable, Path.of(projectPath), PyBundle.message("python.sdk.pipenv.execution.exception.error.running.pipenv.message"), *args)
return runCommand(executable, Path.of(projectPath), PyBundle.message("python.sdk.pipenv.execution.exception.error.running.pipenv.message"), *args).getOrThrow()
}
/**

View File

@@ -6,7 +6,6 @@ import com.intellij.execution.RunCanceledByUserException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessNotCreatedException
import com.intellij.execution.process.ProcessOutput
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.ApplicationManager
@@ -21,12 +20,13 @@ import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.util.SystemProperties
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import com.jetbrains.python.sdk.*
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.SystemDependent
import org.jetbrains.annotations.SystemIndependent
@@ -81,7 +81,7 @@ fun validatePoetryExecutable(poetryExecutable: Path?): ValidationInfo? =
/**
* Runs the configured poetry for the specified Poetry SDK with the associated project path.
*/
internal fun runPoetry(sdk: Sdk, vararg args: String): String {
internal fun runPoetry(sdk: Sdk, vararg args: String): Result<String> {
val projectPath = sdk.associatedModulePath?.let { Path.of(it) }
?: throw PyExecutionException(PyBundle.message("python.sdk.poetry.execution.exception.no.project.message"),
"Poetry", emptyList(), ProcessOutput())
@@ -92,10 +92,10 @@ internal fun runPoetry(sdk: Sdk, vararg args: String): String {
/**
* Runs the configured poetry for the specified project path.
*/
fun runPoetry(projectPath: Path?, vararg args: String): String {
fun runPoetry(projectPath: Path?, vararg args: String): Result<String> {
val executable = getPoetryExecutable()
?: throw PyExecutionException(PyBundle.message("python.sdk.poetry.execution.exception.no.poetry.message"), "poetry",
emptyList(), ProcessOutput())
?: return Result.failure(PyExecutionException(PyBundle.message("python.sdk.poetry.execution.exception.no.poetry.message"), "poetry",
emptyList(), ProcessOutput()))
return runCommand(executable, projectPath, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"), *args)
}
@@ -121,10 +121,11 @@ fun setupPoetry(projectPath: Path, python: String?, installPackages: Boolean, in
python != null -> runPoetry(projectPath, "env", "use", python)
else -> runPoetry(projectPath, "run", "python", "-V")
}
return runPoetry(projectPath, "env", "info", "-p")
return runPoetry(projectPath, "env", "info", "-p").getOrThrow()
}
private fun runCommand(projectPath: Path, command: String, vararg args: String): String {
private fun runCommand(projectPath: Path, command: String, vararg args: String): Result<String> {
val commandLine = GeneralCommandLine(listOf(command) + args).withWorkingDirectory(projectPath)
val handler = CapturingProcessHandler(commandLine)
@@ -134,12 +135,12 @@ private fun runCommand(projectPath: Path, command: String, vararg args: String):
return with(result) {
when {
isCancelled ->
throw RunCanceledByUserException()
Result.failure(RunCanceledByUserException())
exitCode != 0 ->
throw PyExecutionException(PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"), command,
args.asList(),
stdout, stderr, exitCode, emptyList())
else -> stdout
Result.failure(PyExecutionException(PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"), command,
args.asList(),
stdout, stderr, exitCode, emptyList()))
else -> Result.success(stdout)
}
}
}
@@ -149,15 +150,15 @@ internal fun runPoetryInBackground(module: Module, args: List<String>, @NlsSafe
withBackgroundProgress(module.project, "$description...", true) {
val sdk = module.pythonSdk ?: return@withBackgroundProgress
try {
runPoetry(sdk, *args.toTypedArray())
}
catch (e: ExecutionException) {
showSdkExecutionException(sdk, e, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"))
val result = runPoetry(sdk, *args.toTypedArray()).exceptionOrNull()
if (result is ExecutionException) {
showSdkExecutionException(sdk, result, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"))
}
}
finally {
PythonSdkUtil.getSitePackagesDirectory(sdk)?.refresh(true, true)
sdk.associatedModuleDir?.refresh(true, false)
PyPackageManager.getInstance(sdk).refreshAndGetPackages(true)
PythonPackageManager.forSdk(module.project, sdk).reloadPackages()
}
}
}
@@ -168,7 +169,7 @@ internal fun detectPoetryEnvs(module: Module?, existingSdkPaths: Set<String>, pr
return try {
getPoetryEnvs(path).filter { existingSdkPaths.contains(getPythonExecutable(it)) }.map { PyDetectedSdk(getPythonExecutable(it)) }
}
catch (e: Throwable) {
catch (_: Throwable) {
emptyList()
}
}
@@ -191,21 +192,44 @@ inline fun <reified T> syncRunPoetry(
): T {
return try {
ApplicationManager.getApplication().executeOnPooledThread<T> {
try {
val result = runPoetry(projectPath, *args)
callback(result)
}
catch (e: PyExecutionException) {
defaultResult
}
catch (e: ProcessNotCreatedException) {
defaultResult
}
val result = runPoetry(projectPath, *args).getOrNull()
if (result == null) defaultResult else callback(result)
}.get(30, TimeUnit.SECONDS)
}
catch (e: TimeoutException) {
catch (_: TimeoutException) {
defaultResult
}
}
fun getPythonExecutable(homePath: String): String = VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(homePath))?.toString() ?: FileUtil.join(homePath, "bin", "python")
fun getPythonExecutable(homePath: String): String = VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(homePath))?.toString()
?: FileUtil.join(homePath, "bin", "python")
/**
* Installs a Python package using Poetry.
* Runs `poetry add [pkg] [extraArgs]`
*
* @param [pkg] The name of the package to be installed.
* @param [extraArgs] Additional arguments to pass to the Poetry add command.
*/
@Internal
fun poetryInstallPackage(sdk: Sdk, pkg: String, extraArgs: List<String>): Result<String> {
val args = listOf("add", pkg) + extraArgs
return runPoetry(sdk, *args.toTypedArray())
}
/**
* Uninstalls a Python package using Poetry.
* Runs `poetry remove [pkg]`
*
* @param [pkg] The name of the package to be uninstalled.
*/
@Internal
fun poetryUninstallPackage(sdk: Sdk, pkg: String): Result<String> = runPoetry(sdk, "remove", pkg)
@Internal
fun poetryReloadPackages(sdk: Sdk): Result<String> {
runPoetry(sdk, "update").onFailure { return Result.failure(it) }
runPoetry(sdk, "install", "--no-root").onFailure { return Result.failure(it) }
return runPoetry(sdk, "show")
}

View File

@@ -0,0 +1,87 @@
// Copyright 2000-2024 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.sdk.poetry
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.pip.PipBasedPackageManager
import com.jetbrains.python.packaging.pip.PipRepositoryManager
import org.jetbrains.annotations.TestOnly
import java.util.regex.Pattern
class PoetryPackageManager(project: Project, sdk: Sdk) : PipBasedPackageManager(project, sdk) {
@Volatile
private var outdatedPackages: Map<String, PoetryOutdatedVersion> = emptyMap()
override val repositoryManager: PipRepositoryManager = PipRepositoryManager(project, sdk)
override suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List<String>): Result<String> =
poetryInstallPackage(sdk, specification.getVersionForPoetry(), options)
override suspend fun updatePackageCommand(specification: PythonPackageSpecification): Result<String> =
poetryInstallPackage(sdk, specification.getVersionForPoetry(), emptyList())
override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<String> = poetryUninstallPackage(sdk, pkg.name)
override suspend fun reloadPackagesCommand(): Result<List<PythonPackage>> {
val output = poetryReloadPackages(sdk).getOrElse { return Result.failure(it) }
return Result.success(parsePoetryShow(output))
}
override suspend fun reloadPackages(): Result<List<PythonPackage>> {
updateOutdatedPackages()
return super.reloadPackages()
}
internal fun getOutdatedPackages(): Map<String, PoetryOutdatedVersion> = outdatedPackages
/**
* Updates the list of outdated packages by running the Poetry command
* `poetry show --outdated`, parsing its output, and storing the results.
*/
private fun updateOutdatedPackages() {
val outputOutdatedPackages = runPoetry(sdk, "show", "--outdated").getOrElse {
outdatedPackages = emptyMap()
return
}
outdatedPackages = parsePoetryShowOutdated(outputOutdatedPackages)
}
private fun PythonPackageSpecification.getVersionForPoetry(): String = if (versionSpecs == null) name else "$name@$versionSpecs"
}
/**
* Parses the output of `poetry show` into a list of packages.
*/
private 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() }
result.add(PythonPackage(packageInfo[0], packageInfo[1], false))
}
}
return result
}
/**
* Parses the output of `poetry show --outdated` into a list of packages.
*/
private fun parsePoetryShowOutdated(input: String): Map<String, PoetryOutdatedVersion> =
input
.lines()
.map { it.trim() }
.filter { it.isNotBlank() }
.mapNotNull { line ->
line.split(Pattern.compile(" +"))
.takeIf { it.size > 3 }?.let { it[0] to PoetryOutdatedVersion(it[1], it[2]) }
}.toMap()
@TestOnly
fun parsePoetryShowTest(input: String): List<PythonPackage> = parsePoetryShow(input)
@TestOnly
fun parsePoetryShowOutdatedTest(input: String): Map<String, PoetryOutdatedVersion> = parsePoetryShowOutdated(input)

View File

@@ -0,0 +1,14 @@
package com.jetbrains.python.sdk.poetry
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManagerProvider
/**
* This source code is created by @koxudaxi Koudai Aono <koxudaxi@gmail.com>
*/
class PoetryPackageManagerProvider : PythonPackageManagerProvider {
override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = if (sdk.isPoetry) PoetryPackageManager(project, sdk) else null
}

View File

@@ -12,7 +12,7 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiFile
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.sdk.PythonSdkUtil
import org.toml.lang.psi.TomlKeyValue
import org.toml.lang.psi.TomlTable
@@ -47,8 +47,8 @@ internal class PoetryPackageVersionsInspection : LocalInspectionTool() {
it.children.mapNotNull { line -> line as? TomlKeyValue }
}.forEach { keyValue ->
val packageName = keyValue.key.text
val outdatedVersion = (PyPackageManager.getInstance(
sdk) as? PyPoetryPackageManager)?.let { it.getOutdatedPackages()[packageName] }
val outdatedVersion = (PythonPackageManager.forSdk(
module.project, sdk) as? PoetryPackageManager)?.let { it.getOutdatedPackages()[packageName] }
if (outdatedVersion is PoetryOutdatedVersion) {
val message = PyBundle.message("python.sdk.inspection.message.version.outdated.latest",
packageName, outdatedVersion.currentVersion, outdatedVersion.latestVersion)

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 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.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.poetry
import com.google.gson.annotations.SerializedName
@@ -13,6 +13,7 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.*
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.associatedModuleDir
import kotlinx.coroutines.runBlocking
import java.util.regex.Pattern
@@ -57,7 +58,7 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) {
}
try {
runPoetry(sdk, *args.toTypedArray())
runPoetry(sdk, *args.toTypedArray())
}
finally {
sdk.associatedModuleDir?.refresh(true, false)
@@ -69,8 +70,8 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) {
val args = listOf("remove") +
packages.map { it.name }
try {
runPoetry(sdk, *args.toTypedArray())
}
runPoetry(sdk, *args.toTypedArray())
}
finally {
sdk.associatedModuleDir?.refresh(true, false)
refreshAndGetPackages(true)
@@ -102,18 +103,18 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) {
if (alwaysRefresh || packages == null) {
packages = null
val outputInstallDryRun = try {
runPoetry(sdk, "install", "--dry-run", "--no-root")
runPoetry(sdk, "install", "--dry-run", "--no-root")
}
catch (e: ExecutionException) {
packages = emptyList()
return packages ?: emptyList()
}
val allPackage = parsePoetryInstallDryRun(outputInstallDryRun)
val allPackage = parsePoetryInstallDryRun(outputInstallDryRun.getOrThrow())
packages = allPackage.first
requirements = allPackage.second
val outputOutdatedPackages = try {
runPoetry(sdk, "show", "--outdated")
runPoetry(sdk, "show", "--outdated")
}
catch (e: ExecutionException) {
outdatedPackages = emptyMap()

View File

@@ -1,3 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.poetry
import com.intellij.openapi.projectRoots.Sdk

View File

@@ -13,7 +13,7 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
private var packageNames: List<String> = emptyList()
private var packageDetails: PythonPackageDetails? = null
override val installedPackages: List<PythonPackage>
override var installedPackages: List<PythonPackage> = emptyList()
get() = TODO("Not yet implemented")
override val repositoryManager: PythonRepositoryManager
get() = TestPythonRepositoryManager(project, sdk).withPackageNames(packageNames).withPackageDetails(packageDetails)
@@ -28,19 +28,23 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
return this
}
override suspend fun installPackage(specification: PythonPackageSpecification, options: List<String>): Result<List<PythonPackage>> {
TODO("Not yet implemented")
}
override suspend fun updatePackage(specification: PythonPackageSpecification): Result<List<PythonPackage>> {
TODO("Not yet implemented")
}
override suspend fun uninstallPackage(pkg: PythonPackage): Result<List<PythonPackage>> {
TODO("Not yet implemented")
}
override suspend fun reloadPackages(): Result<List<PythonPackage>> {
TODO("Not yet implemented")
}
override suspend fun installPackageCommand(specification: PythonPackageSpecification, options: List<String>): Result<String> {
TODO("Not yet implemented")
}
override suspend fun updatePackageCommand(specification: PythonPackageSpecification): Result<String> {
TODO("Not yet implemented")
}
override suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<String> {
TODO("Not yet implemented")
}
override suspend fun reloadPackagesCommand(): Result<List<PythonPackage>> {
TODO("Not yet implemented")
}
}