PY-78749 Packages: refactor install utils, migrate methods to PPMUI

Signed-off-by: Nikita.Ashihmin <nikita.ashihmin@jetbrains.com>

GitOrigin-RevId: 69eaeb90dd326f7e5cb07d67f615e939980d536b
This commit is contained in:
Nikita.Ashihmin
2025-08-01 15:07:47 +00:00
committed by intellij-monorepo-bot
parent ec7fcf0a8e
commit f5d815d1e4
21 changed files with 1593 additions and 1180 deletions
@@ -114,12 +114,15 @@ sealed class Result<out SUCC, out ERR> {
fun <S> success(value: S): Success<S> = Success(value)
fun <E> failure(error: E): Failure<E> = Failure(error)
fun localizedError(message: @Nls String): Failure<MessageError> = failure(MessageError(message))
@ApiStatus.Internal
inline fun <T> runCatching(body: () -> T): PyResult<T> = pyRunCatching(body)
}
}
@Suppress("UsagesOfObsoleteApi")
@ApiStatus.Internal
fun <T> pyRunCatching(body: () -> T): PyResult<T> {
inline fun <T> pyRunCatching(body: () -> T): PyResult<T> {
return try {
PyResult.success(body())
}
@@ -134,6 +134,7 @@
testServiceImplementation="com.jetbrains.python.codeInsight.completion.DummyRemoteFilePathRetrievalService"/>
<applicationService serviceImplementation="com.jetbrains.python.sdk.PySdkSettings"/>
<applicationService serviceImplementation="com.jetbrains.python.packaging.pip.PypiPackageCache"/>
<applicationService serviceImplementation="com.jetbrains.python.packaging.PyPackageService"/>
<applicationService serviceInterface="com.jetbrains.python.module.PyModuleService"
serviceImplementation="com.jetbrains.python.module.PyModuleServiceImpl"/>
@@ -4,34 +4,25 @@ package com.jetbrains.python.inspections.quickfix
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyPsiPackageUtil.moduleToPackageName
import com.jetbrains.python.packaging.PyPackageInstallUtils
import com.jetbrains.python.packaging.pyRequirement
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.statistics.PyPackagesUsageCollector
import org.jetbrains.annotations.Nls
class InstallAllPackagesQuickFix(private val packageNames: List<String>) : LocalQuickFix {
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement ?: return
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return
val sdk = PythonSdkUtil.findPythonSdk(element) ?: return
val normalizedPackageNames = packageNames.map { moduleToPackageName(it) }
val pyRequirements = normalizedPackageNames.map { pyRequirement(it) }
val confirmedPackages = PyPackageInstallUtils.getConfirmedPackages(pyRequirements, project)
if (confirmedPackages.isEmpty()) return
val fix = PyInstallRequirementsFix(familyName, sdk,
confirmedPackages.toList(),
emptyList())
fix.applyFix(module.project, descriptor)
PyPackagesUsageCollector.installAllEvent.log(confirmedPackages.size)
PyPackageCoroutine.launch(project) {
PythonPackageManagerUI.forSdk(project, sdk).installWithConfirmation(normalizedPackageNames)
}
}
override fun getFamilyName(): @Nls String = PyBundle.message("python.unresolved.reference.inspection.install.all")
@@ -4,13 +4,10 @@ package com.jetbrains.python.inspections.quickfix
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.execution.ExecutionException
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener
import com.jetbrains.python.packaging.PyPackageInstallUtils.confirmInstall
import com.jetbrains.python.packaging.pyRequirement
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.statistics.PyPackagesUsageCollector
import org.jetbrains.annotations.Nls
@@ -19,22 +16,10 @@ internal open class InstallPackageQuickFix(open val packageName: String) : Local
override fun getFamilyName(): @Nls String = PyBundle.message("python.unresolved.reference.inspection.install.package", packageName)
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
if (!confirmInstall(project, packageName)) return
descriptor.psiElement.let { element ->
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return
val sdk = PythonSdkUtil.findPythonSdk(element) ?: return
PyInstallRequirementsFix(
familyName, sdk,
listOf(pyRequirement(packageName)),
listener = object : RunningPackagingTasksListener(module) {
override fun finished(exceptions: List<ExecutionException>) {
onSuccess(descriptor)
}
}
).applyFix(module.project, descriptor)
val sdk = PythonSdkUtil.findPythonSdk(descriptor.psiElement) ?: return
PyPackageCoroutine.launch(project) {
PythonPackageManagerUI.forSdk(project, sdk).installWithConfirmation(listOf(packageName)) ?: return@launch
onSuccess(descriptor)
PyPackagesUsageCollector.installSingleEvent.log()
}
}
@@ -43,11 +28,7 @@ internal open class InstallPackageQuickFix(open val packageName: String) : Local
override fun availableInBatchMode(): Boolean = false
open fun onSuccess(descriptor: ProblemDescriptor?) { }
open fun onSuccess(descriptor: ProblemDescriptor?) {}
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY
companion object {
const val CONFIRM_PACKAGE_INSTALLATION_PROPERTY: String = "python.confirm.package.installation"
}
}
@@ -1,106 +0,0 @@
// 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.inspections.quickfix
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.core.CoreBundle
import com.intellij.model.SideEffectGuard
import com.intellij.model.SideEffectGuard.Companion.checkSideEffectAllowed
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.text.StringUtil
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.inspections.PyInterpreterInspection
import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.management.ui.installPyRequirementsBackground
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.adminPermissionsNeeded
import com.jetbrains.python.ui.PyUiUtil
import org.jetbrains.annotations.Nls
internal class PyInstallRequirementsFix(
private val quickFixName: @Nls String?,
private val sdk: Sdk,
private val unsatisfied: List<PyRequirement>,
private val installOptions: List<String> = emptyList(),
private val listener: RunningPackagingTasksListener? = null,
) : LocalQuickFix {
override fun getFamilyName(): @Nls String = quickFixName ?: PyPsiBundle.message( "QFIX.NAME.install.requirements", unsatisfied.size)
override fun startInWriteAction(): Boolean = false
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
if (hasAdminPermissionsAndConfigureInterpreter(project, descriptor, sdk)) return
checkSideEffectAllowed(SideEffectGuard.EffectType.PROJECT_MODEL)
PyUiUtil.clearFileLevelInspectionResults(descriptor.psiElement.containingFile)
PyPackageCoroutine.launch(project) {
listener?.started()
val pythonPackageManagerUI = PythonPackageManagerUI.forSdk(project, sdk)
pythonPackageManagerUI.installPyRequirementsBackground(unsatisfied, installOptions)
listener?.finished(emptyList())
}
}
private fun hasAdminPermissionsAndConfigureInterpreter(
project: Project,
descriptor: ProblemDescriptor,
sdk: Sdk,
): Boolean {
if (PythonSdkUtil.isRemote(sdk) || !sdk.adminPermissionsNeeded()) return false
val answer = askToConfigureInterpreter(project, sdk)
if (answer == Messages.YES) {
PyInterpreterInspection.ConfigureInterpreterFix().applyFix(project, descriptor)
}
/**
* @return `true` if:
* - The user agreed to configure the interpreter (`Messages.YES`).
* - The user canceled the dialog (`Messages.CANCEL`).
* - The response is indeterminate or invalid (`-1`).
*/
return answer == Messages.YES || answer == Messages.CANCEL || answer == -1
}
/**
* Displays a dialog asking the user to configure an interpreter, proceed with installation, or cancel the operation.
*
* @param project The project in which the dialog is displayed.
* @param sdk The SDK for which the operation is being prompted.
* @return An integer representing the user's choice:
* <ul>
* <li><code>0</code>: The user selected the "Configure" option.</li>
* <li><code>1</code>: The user selected the "Install Anyway" option.</li>
* <li><code>2</code>: The user selected the "Cancel" option.</li>
* </ul>
*/
private fun askToConfigureInterpreter(project: Project, sdk: Sdk): Int {
val sdkName = StringUtil.shortenTextWithEllipsis(sdk.name, SDK_NAME_MAX_LENGTH, 0)
val text = PyPsiBundle.message("INSP.package.requirements.administrator.privileges.required.description", sdkName)
val options = arrayOf(
PyPsiBundle.message("INSP.package.requirements.administrator.privileges.required.button.configure"),
PyPsiBundle.message("INSP.package.requirements.administrator.privileges.required.button.install.anyway"),
CoreBundle.message("button.cancel")
)
return Messages.showIdeaMessageDialog(
project,
text,
PyPsiBundle.message("INSP.package.requirements.administrator.privileges.required"),
options,
DEFAULT_OPTION_INDEX,
Messages.getWarningIcon(),
null
)
}
companion object {
private const val DEFAULT_OPTION_INDEX = 0
private const val SDK_NAME_MAX_LENGTH = 25
}
}
@@ -21,6 +21,7 @@ import com.intellij.psi.util.QualifiedName
import com.intellij.psi.util.parentOfType
import com.intellij.util.containers.ContainerUtil
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyPsiPackageUtil
import com.jetbrains.python.codeInsight.PyCodeInsightSettings
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.codeInsight.imports.AutoImportHintAction
@@ -30,8 +31,9 @@ import com.jetbrains.python.getEffectiveLanguageLevel
import com.jetbrains.python.inspections.PyInspectionVisitor
import com.jetbrains.python.inspections.PyUnresolvedReferenceQuickFixProvider
import com.jetbrains.python.inspections.quickfix.*
import com.jetbrains.python.packaging.PyPackageInstallUtils
import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.isNotInstalledAndCanBeInstalled
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyFromImportStatementImpl
import com.jetbrains.python.psi.impl.PyImportElementImpl
@@ -86,8 +88,8 @@ class PyUnresolvedReferencesInspection : PyUnresolvedReferencesInspectionBase()
if (components.isEmpty()) {
return emptyList()
}
val packageName = PyPsiPackageUtil.moduleToPackageName(components[0])
val packageName = components[0]
val module = ModuleUtilCore.findModuleForPsiElement(node)
val sdk = PythonSdkUtil.findPythonSdk(module)
if (module == null || sdk == null || !PyPackageUtil.packageManagementEnabled(sdk, false, true)) {
@@ -95,9 +97,12 @@ class PyUnresolvedReferencesInspection : PyUnresolvedReferencesInspectionBase()
}
val pyPackage = PyPackageInstallUtils.offeredPackageForNotFoundModule(module.project, sdk, packageName)
val packageCandidates = listOfNotNull(pyPackage)
return packageCandidates.map { pkg: String -> InstallPackageQuickFix(pkg) }
val packageManager = PythonPackageManager.forSdk(module.project, sdk)
val shouldBeSuggest = packageManager.isNotInstalledAndCanBeInstalled(packageName)
if (!shouldBeSuggest)
return emptyList()
return listOfNotNull(InstallPackageQuickFix(packageName))
}
override fun getInstallAllPackagesQuickFix(): InstallAllPackagesQuickFix {
@@ -180,9 +185,11 @@ class PyUnresolvedReferencesInspection : PyUnresolvedReferencesInspectionBase()
return inspectionProfile.getUnwrappedTool(SHORT_NAME_KEY.toString(), element) as PyUnresolvedReferencesInspection?
}
private fun createInstallAndImportQuickFix(project: Project, pythonSdk: Sdk, packageName: String, asName: String?): LocalQuickFix? {
return if (PyPackageInstallUtils.checkShouldToInstallSnapshot(project, pythonSdk, packageName))
InstallAndImportPackageQuickFix(packageName, asName)
private fun createInstallAndImportQuickFix(project: Project, pythonSdk: Sdk, importedModuleName: String, asName: String?): LocalQuickFix? {
val packageName = PyPsiPackageUtil.moduleToPackageName(importedModuleName)
val canBeInstalled = PythonPackageManager.forSdk(project, pythonSdk).isNotInstalledAndCanBeInstalled(packageName)
return if (canBeInstalled)
InstallAndImportPackageQuickFix(importedModuleName, asName)
else
null
}
@@ -1,115 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging
import com.intellij.icons.AllIcons
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.DoNotAskOption
import com.intellij.openapi.ui.MessageDialogBuilder.Companion.yesNo
import com.intellij.openapi.ui.Messages
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyPsiPackageUtil
import com.jetbrains.python.codeInsight.stdlib.PyStdlibUtil
import com.jetbrains.python.inspections.quickfix.InstallPackageQuickFix
import com.jetbrains.python.packaging.PyPIPackageUtil.INSTANCE
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.hasInstalledPackageSnapshot
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.management.ui.installPyRequirementsBackground
import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
import com.jetbrains.python.packaging.ui.PyChooseRequirementsDialog
import com.jetbrains.python.statistics.PyPackagesUsageCollector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
/**
* PyCharm doesn't provide any API for package management for external plugins.
* The closest thing is [PythonPackageManager], although it is also subject to change
*/
@ApiStatus.Internal
object PyPackageInstallUtils {
internal fun getConfirmedPackages(packageNames: List<PyRequirement>, project: Project): Set<PyRequirement> {
val confirmationEnabled = PropertiesComponent.getInstance()
.getBoolean(InstallPackageQuickFix.CONFIRM_PACKAGE_INSTALLATION_PROPERTY, true)
if (!confirmationEnabled || packageNames.isEmpty()) return packageNames.toSet()
val dialog = PyChooseRequirementsDialog(project, packageNames) { it.presentableTextWithoutVersion }
if (!dialog.showAndGet()) {
PyPackagesUsageCollector.installAllCanceledEvent.log()
return emptySet()
}
return dialog.markedElements.toSet()
}
fun offeredPackageForNotFoundModule(project: Project, sdk: Sdk, moduleName: String): String? {
val shouldToInstall = checkShouldToInstallSnapshot(project, sdk, moduleName)
if (!shouldToInstall)
return null
return PyPsiPackageUtil.moduleToPackageName(moduleName)
}
fun checkShouldToInstallSnapshot(project: Project, sdk: Sdk, moduleName: String): Boolean {
val packageName = PyPsiPackageUtil.moduleToPackageName(moduleName)
return !checkIsInstalledSnapshot(project, sdk, packageName) && INSTANCE.isInPyPI(packageName)
}
fun checkIsInstalledSnapshot(project: Project, sdk: Sdk, packageName: String): Boolean {
val isStdLib = PyStdlibUtil.getPackages()?.contains(packageName) ?: false
if (isStdLib) {
return true
}
val packageManager = PythonPackageManager.forSdk(project, sdk)
return packageManager.hasInstalledPackageSnapshot(packageName)
}
suspend fun confirmAndInstall(project: Project, sdk: Sdk, requirement: PyRequirement) {
val isConfirmed = withContext(Dispatchers.EDT) {
confirmInstall(project, requirement.name)
}
if (!isConfirmed)
return
PythonPackageManagerUI.forSdk(project, sdk).installPyRequirementsBackground(listOf(requirement))
}
suspend fun confirmAndInstall(project: Project, sdk: Sdk, packageName: String, versionSpec: PyRequirementVersionSpec? = null) {
confirmAndInstall(project, sdk, pyRequirement(packageName, versionSpec))
}
fun confirmInstall(project: Project, packageName: String): Boolean {
val isWellKnownPackage = ApplicationManager.getApplication()
.getService(PyPIPackageRanking::class.java)
.packageRank.containsKey(packageName)
val confirmationEnabled = PropertiesComponent.getInstance().getBoolean(InstallPackageQuickFix.CONFIRM_PACKAGE_INSTALLATION_PROPERTY, true)
if (!isWellKnownPackage && confirmationEnabled) {
val confirmed: Boolean = yesNo(PyBundle.message("python.packaging.dialog.title.install.package.confirmation"),
PyBundle.message("python.packaging.dialog.message.install.package.confirmation", packageName))
.icon(AllIcons.General.WarningDialog)
.doNotAsk(ConfirmPackageInstallationDoNotAskOption())
.ask(project)
if (!confirmed) {
return false
}
}
return true
}
private class ConfirmPackageInstallationDoNotAskOption : DoNotAskOption.Adapter() {
override fun rememberChoice(isSelected: Boolean, exitCode: Int) {
if (isSelected && exitCode == Messages.OK) {
PropertiesComponent.getInstance().setValue(InstallPackageQuickFix.CONFIRM_PACKAGE_INSTALLATION_PROPERTY, false, true)
}
}
}
}
@@ -39,6 +39,11 @@ fun PythonPackageManager.hasInstalledPackageSnapshot(packageName: String, versio
getInstalledPackageSnapshot(packageName, version) != null
@ApiStatus.Internal
fun PythonPackageManager.isNotInstalledAndCanBeInstalled(packageName: String, version: String? = null): Boolean =
!hasInstalledPackageSnapshot(packageName, version) && repositoryManager.hasPackageSnapshot(packageName)
@ApiStatus.Internal
suspend fun PythonPackageManager.findPackageSpecification(
packageName: String,
@@ -35,6 +35,11 @@ interface PythonRepositoryManager {
return repository.getPackages().filter { StringUtil.containsIgnoreCase(normalizePackageName(it), normalizedQuery) }
}
fun hasPackageSnapshot(packageName: String): Boolean {
return repositories.any { packageName in it.getPackages() }
}
suspend fun findPackageSpecification(
requirement: PyRequirement,
repository: PyPackageRepository? = null,
@@ -0,0 +1,72 @@
// Copyright 2000-2025 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.ui
import com.intellij.icons.AllIcons
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DoNotAskOption
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.ui.Messages
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.ui.PyChooseRequirementsDialog
import com.jetbrains.python.statistics.PyPackagesUsageCollector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal object PyPackageManagerUiConfirmationHelpers {
private const val CONFIRM_PACKAGE_INSTALLATION_PROPERTY: String = "python.confirm.package.installation"
internal suspend fun getConfirmedPackages(pyRequirements: List<PyRequirement>, project: Project): List<PyRequirement> {
if (pyRequirements.isEmpty())
return emptyList()
val confirmationEnabled = PropertiesComponent.getInstance()
.getBoolean(CONFIRM_PACKAGE_INSTALLATION_PROPERTY, true)
if (!confirmationEnabled)
return pyRequirements
if (pyRequirements.size == 1) {
val pyPackage = pyRequirements.first()
if (askSingleFileConfirmation(pyPackage, project)) {
return listOf(pyPackage)
}
else {
PyPackagesUsageCollector.installAllCanceledEvent.log()
return listOf()
}
}
return withContext(Dispatchers.EDT) {
val dialog = PyChooseRequirementsDialog(project, pyRequirements) { it.presentableText }
val result = dialog.showAndGet()
if (!result) {
PyPackagesUsageCollector.installAllCanceledEvent.log()
listOf()
}
else {
dialog.markedElements
}
}
}
private suspend fun askSingleFileConfirmation(pyRequirement: PyRequirement, project: Project): Boolean = withContext(Dispatchers.EDT) {
MessageDialogBuilder.yesNo(
PyBundle.message("python.packaging.dialog.title.install.package.confirmation"),
PyBundle.message("python.packaging.dialog.message.install.package.confirmation", pyRequirement.presentableText))
.icon(AllIcons.General.WarningDialog)
.doNotAsk(ConfirmPackageInstallationDoNotAskOption())
.ask(project)
}
private class ConfirmPackageInstallationDoNotAskOption : DoNotAskOption.Adapter() {
override fun rememberChoice(isSelected: Boolean, exitCode: Int) {
if (isSelected && exitCode == Messages.OK) {
PropertiesComponent.getInstance().setValue(CONFIRM_PACKAGE_INSTALLATION_PROPERTY, false, true)
}
}
}
}
@@ -6,10 +6,13 @@ import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.pyRequirement
import com.jetbrains.python.statistics.PyPackagesUsageCollector
import com.jetbrains.python.util.ShowingMessageErrorSync
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
@@ -43,6 +46,22 @@ class PythonPackageManagerUI(val manager: PythonPackageManager, val sink: ErrorS
}
}
suspend fun installWithConfirmation(packages: List<String>): List<PythonPackage>? {
val requirements = packages.map { pyRequirement(it) }
return installPyRequirementsWithConfirmation(requirements)
}
suspend fun installPyRequirementsWithConfirmation(packages: List<PyRequirement>): List<PythonPackage>? {
val confirmed = PyPackageManagerUiConfirmationHelpers.getConfirmedPackages(packages, project)
if (confirmed.isEmpty())
return null
PyPackagesUsageCollector.installAllEvent.log(confirmed.size)
return installPyRequirementsBackground(confirmed)
}
/**
* @return List of all installed packages or null if the operation was failed.
*/
@@ -102,20 +121,13 @@ class PythonPackageManagerUI(val manager: PythonPackageManager, val sink: ErrorS
}
}
suspend fun syncBackground() {
val progressTitle = PyBundle.message("python.packaging.sync.packages")
executeCommand(progressTitle) {
manager.sync()
}
}
@ApiStatus.Internal
suspend fun <T> executeCommand(
progressTitle: @Nls String,
operation: suspend (() -> PyResult<T>?),
): T? = PythonPackageManagerUIHelpers.runPackagingOperationMaybeBackground(manager.project, sink, progressTitle) {
operation()
operation()
}
companion object {
@@ -3,7 +3,6 @@ package com.jetbrains.python.packaging.management.ui
import com.intellij.ui.awt.RelativePoint
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageInstallUtils.confirmAndInstall
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
@@ -42,7 +41,7 @@ fun PythonPackageManagerUI.launchInstallPackageWithBalloonBackground(packageName
val loadBalloon = PythonPackageManagerUIHelpers.showBalloon(point, PyBundle.message("python.packaging.installing.package", packageName),
PythonPackageManagerUIHelpers.BalloonStyle.INFO)
try {
confirmAndInstall(project, sdk, packageName, versionSpec = versionSpec)
installPyRequirementsWithConfirmation(listOf(pyRequirement(packageName, versionSpec)))
loadBalloon.hide()
PyPackagesUsageCollector.installPackageFromConsole.log(project)
PythonPackageManagerUIHelpers.showBalloon(point, PyBundle.message("python.packaging.notification.description.installed.packages", packageName), PythonPackageManagerUIHelpers.BalloonStyle.SUCCESS)
@@ -13,7 +13,6 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.io.IdeUtilIoBundle
import com.intellij.util.net.HttpConfigurable
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.errorProcessing.PyResult
@@ -27,6 +26,7 @@ import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManagerEngine
import com.jetbrains.python.packaging.management.PythonPackageManagerRunner
import com.jetbrains.python.packaging.utils.PyProxyUtils
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.run.buildTargetedCommandLine
import com.jetbrains.python.run.ensureProjectSdkAndModuleDirsAreOnTarget
@@ -120,7 +120,7 @@ class PipPackageManagerEngine(
pythonExecution.addParameter(operation)
if (operation == "install") {
proxyString?.let {
PyProxyUtils.proxyString?.let {
pythonExecution.addParameter("--proxy")
pythonExecution.addParameter(it)
}
@@ -182,15 +182,6 @@ class PipPackageManagerEngine(
return@withContext PyResult.success(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
}
private fun partitionPackagesBySource(installRequest: PythonPackageInstallRequest): List<List<String>> {
when (installRequest) {
@@ -37,8 +37,7 @@ private val LOG = logger<PypiPackageCache>()
private val ALPHABET_REGEX = Regex("[-a-z0-9]+")
@ApiStatus.Internal
@Service
class PypiPackageCache : PythonPackageCache<String> {
open class PypiPackageCache : PythonPackageCache<String> {
override val packages: Set<String>
get() = cache
@@ -63,7 +62,7 @@ class PypiPackageCache : PythonPackageCache<String> {
val filePath: Path = Paths.get(PathManager.getSystemPath(), "python_packages", "packages_v2.json")
@CheckReturnValue
suspend fun reloadCache(force: Boolean = false): Result<Unit, IOException> {
open suspend fun reloadCache(force: Boolean = false): Result<Unit, IOException> {
lock.withLock {
if ((cache.isNotEmpty() && !force) || loadInProgress) {
return Result.success(Unit)
@@ -93,7 +92,7 @@ class PypiPackageCache : PythonPackageCache<String> {
return@withContext false
}
var packageList = emptySet<String>()
var packageList: Set<String>
try {
val type = object : TypeToken<LinkedHashSet<String>>() {}.type
packageList = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)
@@ -0,0 +1,16 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.utils
import com.intellij.util.net.HttpConfigurable
internal object PyProxyUtils {
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
}
}
@@ -6,12 +6,10 @@ import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageInstallUtils
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.management.ui.installPyRequirementsBackground
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.requirements.getPythonSdk
import com.jetbrains.python.sdk.PythonSdkUtil
internal class InstallAllRequirementsQuickFix(val requirements: List<PyRequirement>) : LocalQuickFix {
override fun getFamilyName(): String {
@@ -19,17 +17,9 @@ internal class InstallAllRequirementsQuickFix(val requirements: List<PyRequireme
}
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val confirmedPackages = PyPackageInstallUtils.getConfirmedPackages(requirements, project)
if (confirmedPackages.isEmpty())
return
val file = descriptor.psiElement.containingFile ?: return
val sdk = PythonSdkUtil.findPythonSdk(descriptor.psiElement) ?: return
PyPackageCoroutine.launch(project) {
val sdk = getPythonSdk(file) ?: return@launch
val manager = PythonPackageManagerUI.forSdk(project, sdk)
manager.installPyRequirementsBackground(confirmedPackages.toList(), emptyList<String>()
)
PythonPackageManagerUI.forSdk(project, sdk).installPyRequirementsWithConfirmation(requirements)
}
}
@@ -6,8 +6,8 @@ import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageInstallUtils
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.requirements.getPythonSdk
@@ -20,7 +20,7 @@ internal class InstallRequirementQuickFix(val requirement: PyRequirement) : Loca
val pythonSdk = getPythonSdk(descriptor.psiElement.containingFile) ?: return
PyPackageCoroutine.launch(project) {
PyPackageInstallUtils.confirmAndInstall(project, pythonSdk, requirement)
PythonPackageManagerUI.forSdk(project, pythonSdk).installPyRequirementsWithConfirmation(listOf(requirement))
}
}
@@ -7,7 +7,9 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.psi.PsiDocumentManager
import com.jetbrains.python.packaging.PyPackageInstallUtils
import com.jetbrains.python.PyPsiPackageUtil
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.isNotInstalledAndCanBeInstalled
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
@@ -21,8 +23,15 @@ class PythonInstallPackageFilter(val project: Project, var editor: EditorImpl? =
val moduleName = line.removePrefix(prefix).dropLastWhile { it != '\'' }.dropLast(1)
val pythonSdk = getSdkForFile(editor) ?: project.pythonSdk ?: project.modules.firstNotNullOfOrNull { it.pythonSdk } ?: return null
val pipPackageName = PyPackageInstallUtils.offeredPackageForNotFoundModule(project, pythonSdk, moduleName) ?: return null
val info = InstallPackageButtonItem(project, pythonSdk, entireLength - line.length + "ModuleNotFoundError:".length, pipPackageName)
val packageManager = PythonPackageManager.forSdk(project, pythonSdk)
val packageName = PyPsiPackageUtil.moduleToPackageName(moduleName)
val isCanBeInstalled = packageManager.isNotInstalledAndCanBeInstalled(packageName)
if (!isCanBeInstalled)
return null
val info = InstallPackageButtonItem(project, pythonSdk, entireLength - line.length + "ModuleNotFoundError:".length, packageName)
return Filter.Result(
listOf(
info,
@@ -1,867 +0,0 @@
// Copyright 2000-2017 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;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ex.InspectionProfileImpl;
import com.intellij.openapi.application.impl.NonBlockingReadActionImpl;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.TestDataFile;
import com.intellij.testFramework.TestDataPath;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.codeInsight.PyCodeInsightSettings;
import com.jetbrains.python.documentation.docstrings.DocStringFormat;
import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.inspections.*;
import com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesInspection;
import com.jetbrains.python.packaging.PyPIPackageCache;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.quickFixes.PyRenameElementQuickFixTest;
import org.intellij.lang.regexp.inspection.RegExpRedundantEscapeInspection;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@TestDataPath("$CONTENT_ROOT/../testData/inspections/")
public class PyQuickFixTest extends PyTestCase {
@Override
protected @Nullable LightProjectDescriptor getProjectDescriptor() {
return ourPy2Descriptor;
}
@Override
protected void setUp() throws Exception {
super.setUp();
InspectionProfileImpl.INIT_INSPECTIONS = true;
myFixture.setCaresAboutInjection(false);
PyRenameElementQuickFixTest.registerTestNameSuggestionProvider(getTestRootDisposable());
}
@Override
protected void tearDown() throws Exception {
InspectionProfileImpl.INIT_INSPECTIONS = false;
super.tearDown();
}
public void testAddImport() {
doInspectionTest(new String[]{"AddImport.py", "ImportTarget.py"}, PyUnresolvedReferencesInspection.class,
"Import 'ImportTarget'", true, true);
}
public void testAddImportDoc() {
doInspectionTest(new String[]{"AddImportDoc.py", "ImportTarget.py"}, PyUnresolvedReferencesInspection.class,
"Import 'ImportTarget'", true, true);
}
// PY-728
public void testAddImportDocComment() {
doInspectionTest(new String[]{"AddImportDocComment.py", "ImportTarget.py"}, PyUnresolvedReferencesInspection.class,
"Import 'ImportTarget'", true, true);
}
// PY-42307
public void testInstallAndImportPackageByNameAlias() {
//noinspection removal
PyPIPackageCache.reload(List.of("pandas", "pd"));
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
myFixture.configureByText(PythonFileType.INSTANCE, "pd<caret>.array()");
myFixture.findSingleIntention("Import 'turtle.pd'"); // standard library
myFixture.findSingleIntention("Install and import package 'pd'"); // PyPI
myFixture.findSingleIntention("Install and import package 'pandas'"); // 'pd' is a common import alias for 'pandas' from PyPI
}
public void testImportFromModule() {
doInspectionTest(new String[]{"importFromModule/foo/bar.py", "importFromModule/foo/baz.py", "importFromModule/foo/__init__.py"},
PyUnresolvedReferencesInspection.class, "Import 'importFromModule.foo.baz'", true, true);
}
// PY-14365
public void testObjectBaseIsNotShownInAutoImportQuickfix() {
myFixture.copyDirectoryToProject("objectBaseIsNotShownInAutoImportQuickfix", "");
myFixture.configureByFile("main.py");
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
final IntentionAction intention = myFixture.findSingleIntention("Import");
assertNotNull(intention);
assertEquals("Import 'module.MyOldStyleClass'", intention.getText());
}
// PY-6302
public void testImportFromModuleStar() {
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
myFixture.copyDirectoryToProject("importFromModuleStar", "");
myFixture.configureFromTempProjectFile("source.py");
myFixture.checkHighlighting(true, false, false);
final IntentionAction intentionAction = myFixture.findSingleIntention("Import 'target.xyzzy'");
assertNotNull(intentionAction);
myFixture.launchAction(intentionAction);
myFixture.checkResultByFile("importFromModuleStar/source_after.py");
}
public void testQualifyByImport() {
final PyCodeInsightSettings settings = PyCodeInsightSettings.getInstance();
boolean oldPreferFrom = settings.PREFER_FROM_IMPORT;
settings.PREFER_FROM_IMPORT = false;
try {
doInspectionTest(new String[]{"QualifyByImport.py", "QualifyByImportFoo.py"}, PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("ACT.qualify.with.module"), true, true);
}
finally {
settings.PREFER_FROM_IMPORT = oldPreferFrom;
}
}
public void testAddToImportFromList() {
doInspectionTest(new String[]{"AddToImportFromList.py", "AddToImportFromFoo.py"}, PyUnresolvedReferencesInspection.class,
"Import 'add_to_import_test_unique_name from AddToImportFromFoo'", true, true);
}
// TODO: add a test for multiple variants of above
// TODO: add tests for stub indexes-based autoimport of unimported somehow.
public void testAddSelf() {
doInspectionTest(PyMethodParametersInspection.class, PyPsiBundle.message("QFIX.add.parameter.self", "self"), true, true);
}
public void testReplacePrint() {
doInspectionTest(PyCompatibilityInspection.class, PyPsiBundle.message("QFIX.statement.effect"), true, true);
}
// PY-22045
public void testBatchReplacePrintInsertsFutureImportOnlyOnce() {
doInspectionTest(
PyCompatibilityInspection.class,
"Fix all 'Code is incompatible with specific Python versions' problems in file",
true,
true
);
}
// PY-4556
public void testAddSelfFunction() {
doInspectionTest("AddSelfFunction.py", PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.unresolved.reference", "get_a", "self"), true, true);
}
// PY-9721
public void testAddSelfToClassmethod() {
doInspectionTest("AddSelfToClassmethod.py", PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.unresolved.reference", "foo", "cls"), true, true);
}
public void testAddCls() {
doInspectionTest(PyMethodParametersInspection.class, PyPsiBundle.message("QFIX.add.parameter.self", "cls"), true, true);
}
public void testRenameToSelf() {
doInspectionTest(PyMethodParametersInspection.class, PyPsiBundle.message("QFIX.rename.parameter", "self"), true, true);
}
public void testRemoveTrailingSemicolon() {
doInspectionTest(PyTrailingSemicolonInspection.class, PyPsiBundle.message("QFIX.remove.trailing.semicolon"), true, true);
}
public void testDictCreation() {
doInspectionTest(PyDictCreationInspection.class, PyPsiBundle.message("QFIX.dict.creation"), true, true);
}
// PY-6283
public void testDictCreationTuple() {
doInspectionTest(PyDictCreationInspection.class, PyPsiBundle.message("QFIX.dict.creation"), true, true);
}
// PY-7318
public void testDictCreationDuplicate() {
doInspectionTest(PyDictCreationInspection.class, PyPsiBundle.message("QFIX.dict.creation"), true, true);
}
// PY-40177
public void testDictCreationWithDoubleStars() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doInspectionTest(PyDictCreationInspection.class, PyPsiBundle.message("QFIX.dict.creation"), true, true));
}
public void testTransformClassicClass() {
doInspectionTest(PyClassicStyleClassInspection.class, PyPsiBundle.message("QFIX.classic.class.transform"), true, true);
}
public void testAddGlobalStatement() {
doInspectionTest(PyUnboundLocalVariableInspection.class, PyPsiBundle.message("QFIX.add.global"), true, true);
}
public void testAddGlobalExistingStatement() {
doInspectionTest(PyUnboundLocalVariableInspection.class, PyPsiBundle.message("QFIX.add.global"), true, true);
}
public void testSimplifyBooleanCheck() {
doInspectionTest(PySimplifyBooleanCheckInspection.class, PyPsiBundle.message("QFIX.simplify.boolean.expression", "b"), true, true);
}
public void testMoveFromFutureImport() {
doInspectionTest(PyFromFutureImportInspection.class, PyPsiBundle.message("QFIX.move.from.future.import"), true, true);
}
// PY-10080
public void testMoveFromFutureImportDocString() {
doInspectionTest(PyFromFutureImportInspection.class, PyPsiBundle.message("QFIX.move.from.future.import"), true, true);
}
// PY-23475
public void testMoveFromFutureImportAboveModuleLevelDunder() {
doInspectionTest(PyFromFutureImportInspection.class, PyPsiBundle.message("QFIX.move.from.future.import"), true, true);
}
public void testComparisonWithNone() {
doInspectionTest(PyComparisonWithNoneInspection.class, PyPsiBundle.message("QFIX.replace.equality"), true, true);
}
public void testAddClassFix() {
doInspectionTest("AddClass.py", PyUnresolvedReferencesInspection.class, "Create class 'Xyzzy'", true, true);
}
// PY-42389
public void testAddClassFixPython3() {
runWithLanguageLevel(LanguageLevel.getLatest(), () ->
doInspectionTest(PyUnresolvedReferencesInspection.class, "Create class 'Xyzzy'", true, true));
}
// PY-21204
public void testAddClassFromTypeComment() {
doInspectionTest(PyUnresolvedReferencesInspection.class, "Create class 'MyClass'", true, true);
}
// PY-21204
public void testAddClassFromFString() {
runWithLanguageLevel(LanguageLevel.PYTHON36,
() -> doInspectionTest(PyUnresolvedReferencesInspection.class, "Create class 'MyClass'", true, true));
}
// PY-33802
public void testAddClassToImportedModule() {
doMultiFilesInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.create.class.in.module", "Clzz", "mod.py"), "mod.py");
}
// PY-33802
public void testAddClassToImportedPackage() {
doMultiFilesInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.create.class.in.module", "Clzz", "__init__.py"), "pkg/__init__.py");
}
// PY-33802
public void testAddClassToModuleInFromImport() {
doMultiFilesInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.create.class.in.module", "Clzz", "mod.py"), "mod.py");
}
// PY-33802
public void testAddClassToPackageInFromImport() {
doMultiFilesInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.create.class.in.module", "Clzz", "__init__.py"),
"mypack/__init__.py");
}
// PY-21204
public void testAddFunctionFromFString() {
runWithLanguageLevel(LanguageLevel.PYTHON36,
() -> doInspectionTest(PyUnresolvedReferencesInspection.class, PyPsiBundle.message("QFIX.NAME.unresolved.reference.create.function", "my_function"), true, true));
}
// PY-1465
public void testAddFunctionToModuleInImport() {
doMultiFilesInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.create.function.in.module", "func", "mod.py"),
"mod.py");
}
// PY-34710
public void testAddFunctionToModuleInFromImport() {
doMultiFilesInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.create.function.in.module", "foo", "mod.py"),
"mod.py");
}
// PY-34710
public void testAddFunctionToPackageInFromImport() {
doMultiFilesInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.create.function.in.module", "foo", "__init__.py"),
"mypack/__init__.py");
}
// PY-1470
public void testRedundantParentheses() {
String[] testFiles = {"RedundantParentheses.py"};
myFixture.enableInspections(PyRedundantParenthesesInspection.class);
myFixture.configureByFiles(testFiles);
myFixture.checkHighlighting(true, false, true);
final IntentionAction intentionAction = myFixture.findSingleIntention(PyPsiBundle.message("QFIX.redundant.parentheses"));
assertNotNull(intentionAction);
myFixture.launchAction(intentionAction);
myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after"));
}
// PY-3095
public void testRedundantParenthesesBoolean() {
doInspectionTest(PyRedundantParenthesesInspection.class, PyPsiBundle.message("QFIX.redundant.parentheses"), true, true);
}
// PY-3239
public void testRedundantParenthesesMore() {
doInspectionTest(PyRedundantParenthesesInspection.class, PyPsiBundle.message("QFIX.redundant.parentheses"), true, true);
}
// PY-12679
public void testRedundantParenthesesParenthesizedExpression() {
doInspectionTest(PyRedundantParenthesesInspection.class, PyPsiBundle.message("QFIX.redundant.parentheses"), true, true);
}
public void testRedundantParenthesesMultipleParentheses() {
doInspectionTest(PyRedundantParenthesesInspection.class, PyPsiBundle.message("QFIX.redundant.parentheses"), true, true);
}
// PY-15506
public void testEmptyListOfBaseClasses() {
doInspectionTest(PyRedundantParenthesesInspection.class, PyPsiBundle.message("QFIX.redundant.parentheses"), true, true);
}
// PY-18203
public void testRedundantParenthesesInTuples() {
doInspectionTest(PyRedundantParenthesesInspection.class, PyPsiBundle.message("QFIX.redundant.parentheses"), true, true);
}
// PY-1020
public void testChainedComparisons() {
doInspectionTest(PyChainedComparisonsInspection.class, PyPsiBundle.message("QFIX.chained.comparison"), true, true);
}
// PY-3126
public void testChainedComparison1() {
doInspectionTest(PyChainedComparisonsInspection.class, PyPsiBundle.message("QFIX.chained.comparison"), true, true);
}
// PY-3126
public void testChainedComparison2() {
doInspectionTest(PyChainedComparisonsInspection.class, PyPsiBundle.message("QFIX.chained.comparison"), true, true);
}
// PY-3126
public void testChainedComparison3() {
doInspectionTest(PyChainedComparisonsInspection.class, PyPsiBundle.message("QFIX.chained.comparison"), true, true);
}
// PY-5623
public void testChainedComparison4() {
doInspectionTest(PyChainedComparisonsInspection.class, PyPsiBundle.message("QFIX.chained.comparison"), true, true);
}
// PY-6467
public void testChainedComparison5() {
doInspectionTest(PyChainedComparisonsInspection.class, PyPsiBundle.message("QFIX.chained.comparison"), true, true);
}
// PY-20004
public void testChainedComparison7() {
doInspectionTest(PyChainedComparisonsInspection.class, PyPsiBundle.message("QFIX.chained.comparison"), true, true);
}
// PY-14002
public void testChainedComparisonWithCommonBinaryExpression() {
doInspectionTest(PyChainedComparisonsInspection.class, PyPsiBundle.message("QFIX.chained.comparison"), true, true);
}
// PY-19583
public void testChainedComparison6() {
doInspectionTest(PyChainedComparisonsInspection.class, "Simplify chained comparison", true, true);
}
// PY-24942
public void testChainedComparison8() {
doInspectionTest(PyChainedComparisonsInspection.class, "Simplify chained comparison", true, true);
}
// PY-29121
public void testChainedComparison9() {
doInspectionTest(PyChainedComparisonsInspection.class, "Simplify chained comparison", true, true);
}
// PY-1362, PY-2585
public void testStatementEffect() {
doInspectionTest(PyStatementEffectInspection.class, PyPsiBundle.message("QFIX.statement.effect"), true, true);
}
// PY-1265
public void testStatementEffectIntroduceVariable() {
doInspectionTest(PyStatementEffectInspection.class, PyPsiBundle.message("QFIX.introduce.variable"), true, true);
}
// PY-2092
public void testUnresolvedRefCreateFunction() {
doInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.NAME.unresolved.reference.create.function", "ref"), true, true);
}
public void testUnresolvedRefCreateAsyncFunction() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.NAME.unresolved.reference.create.function", "ref"), true, true));
}
public void testUnresolvedRefNoCreateFunction() {
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
myFixture.configureByFile("UnresolvedRefNoCreateFunction.py");
myFixture.checkHighlighting(true, false, false);
final IntentionAction intentionAction = myFixture.getAvailableIntention(PyPsiBundle.message("QFIX.NAME.unresolved.reference.create.function", "ref"));
assertNull(intentionAction);
}
public void testReplaceNotEqOperator() {
doInspectionTest(PyCompatibilityInspection.class, PyPsiBundle.message("INTN.replace.noteq.operator"), true, true);
}
public void testListCreation() {
doInspectionTest(PyListCreationInspection.class, PyPsiBundle.message("QFIX.list.creation"), true, true);
}
// PY-16194
public void testListCreationOnlyConsecutiveAppends() {
doInspectionTest(PyListCreationInspection.class, PyPsiBundle.message("QFIX.list.creation"), true, true);
}
// PY-1445
public void testConvertSingleQuotedDocstring() {
getIndentOptions().INDENT_SIZE = 2;
doInspectionTest(PySingleQuotedDocstringInspection.class, PyPsiBundle.message("QFIX.convert.single.quoted.docstring"), true, true);
}
// PY-8926
public void testConvertSingleQuotedDocstringEscape() {
getIndentOptions().INDENT_SIZE = 2;
doInspectionTest(PySingleQuotedDocstringInspection.class, PyPsiBundle.message("QFIX.convert.single.quoted.docstring"), true, true);
}
// PY-3127
public void testDefaultArgument() {
doInspectionTest(PyDefaultArgumentInspection.class, PyPsiBundle.message("QFIX.default.argument"), true, true);
}
public void testDefaultArgumentEmptyList() {
doInspectionTest(PyDefaultArgumentInspection.class, PyPsiBundle.message("QFIX.default.argument"), true, true);
}
// PY-17392
public void testDefaultArgumentCommentsInsideParameters() {
doInspectionTest(PyDefaultArgumentInspection.class, PyPsiBundle.message("QFIX.default.argument"), true, true);
}
// PY-3125
public void testArgumentEqualDefault() {
doInspectionTest(PyArgumentEqualDefaultInspection.class, PyPsiBundle.message("QFIX.remove.argument.equal.default"), true, true);
}
// PY-3315
public void testAddCallSuper() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-4017
public void testAddCallSuper1() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-8654
public void testAddCallSuperPass() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-15867
public void testAddCallSuperOptionalAndRequiredParamsNameCollision() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-15927
public void testAddCallSuperConflictingTupleParam() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-16036
public void testAddCallSuperSelfNamePreserved() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-16420
public void testAddCallSuperRepeatedOptionalParamsPassedToSuperConstructor() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-16420
public void testAddCallSuperRepeatedOptionalTupleParamsPassedToSuperConstructor() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-16289
public void testAddCallSuperCommentAfterColonPreserved() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-16348
public void testAddCallSuperCommentsInFunctionBodyPreserved() {
doInspectionTest(PyMissingConstructorInspection.class, PyPsiBundle.message("QFIX.add.super"), true, true);
}
// PY-491, PY-13297
public void testAddEncoding() {
doInspectionTest(PyMandatoryEncodingInspection.class, PyPsiBundle.message("QFIX.add.encoding"), true, true);
}
// PY-13297
public void testAddEncodingAtLastLine() {
doInspectionTest(PyMandatoryEncodingInspection.class, PyPsiBundle.message("QFIX.add.encoding"), true, true);
}
// PY-3348
public void testRemoveDecorator() {
doInspectionTest(PyDecoratorInspection.class, PyPsiBundle.message("QFIX.remove.decorator"), true, true);
}
public void testAddParameter() {
doInspectionTest(PyUnresolvedReferencesInspection.class,
PyPsiBundle.message("QFIX.unresolved.reference.add.param", "test"), true, true);
}
// PY-6595
public void testRenameUnresolvedReference() {
doInspectionTest(PyUnresolvedReferencesInspection.class, PyPsiBundle.message("QFIX.rename.unresolved.reference"), true, true);
}
// PY-3120
public void testSetFunctionToLiteral() {
runWithLanguageLevel(LanguageLevel.PYTHON27, () -> doInspectionTest(PySetFunctionToLiteralInspection.class, PyPsiBundle.message("QFIX.replace.function.set.with.literal"), true, true));
}
// PY-3394
public void testDocstringParams() {
getIndentOptions().INDENT_SIZE = 2;
runWithDocStringFormat(DocStringFormat.REST,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.add.parameter", "b"), true, true));
}
public void testDocstringParams1() {
getIndentOptions().INDENT_SIZE = 2;
runWithDocStringFormat(DocStringFormat.REST,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "c"), true, true));
}
// PY-4964
public void testDocstringParams2() {
runWithDocStringFormat(DocStringFormat.REST,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.add.parameter", "ham"), true, true));
}
// PY-9795
public void testGoogleDocStringAddParam() {
runWithDocStringFormat(DocStringFormat.GOOGLE,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.add.parameter", "b"), true, true));
}
// PY-9795
public void testGoogleDocStringRemoveParam() {
runWithDocStringFormat(DocStringFormat.GOOGLE,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "c"), true, true));
}
// PY-9795
public void testGoogleDocStringRemoveParamWithSection() {
runWithDocStringFormat(DocStringFormat.GOOGLE,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "c"), true, true));
}
// PY-16761
public void testGoogleDocStringRemovePositionalVararg() {
runWithDocStringFormat(DocStringFormat.GOOGLE,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "args"), true, true));
}
// PY-16761
public void testGoogleDocStringRemoveKeywordVararg() {
runWithDocStringFormat(DocStringFormat.GOOGLE, () -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "kwargs"), true, true));
}
// PY-16908
public void testNumpyDocStringRemoveFirstOfCombinedParams() {
runWithDocStringFormat(DocStringFormat.NUMPY,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "x"), true, true));
}
// PY-16908
public void testNumpyDocStringRemoveMidOfCombinedParams() {
runWithDocStringFormat(DocStringFormat.NUMPY,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "y"), true, true));
}
// PY-16908
public void testNumpyDocStringRemoveLastOfCombinedParams() {
runWithDocStringFormat(DocStringFormat.NUMPY,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "z"), true, true));
}
// PY-16908
public void testNumpyDocStringRemoveCombinedVarargParam() {
runWithDocStringFormat(DocStringFormat.NUMPY,
() -> doInspectionTest(PyIncorrectDocstringInspection.class, PyPsiBundle.message("QFIX.docstring.remove.parameter", "args"), true, true));
}
public void testUnnecessaryBackslash() {
String[] testFiles = {"UnnecessaryBackslash.py"};
myFixture.enableInspections(PyUnnecessaryBackslashInspection.class);
myFixture.configureByFiles(testFiles);
myFixture.checkHighlighting(true, false, true);
IntentionAction intentionAction = myFixture.getAvailableIntention(PyPsiBundle.message("QFIX.remove.unnecessary.backslash"));
assertNotNull(intentionAction);
myFixture.launchAction(intentionAction);
myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after"));
}
// PY-3051
public void testUnresolvedRefTrueFalse() {
doInspectionTest(PyUnresolvedReferencesInspection.class, PyPsiBundle.message("QFIX.replace.with.true.or.false", "True"), true, true);
}
public void testUnnecessaryBackslashInArgumentList() {
String[] testFiles = {"UnnecessaryBackslashInArguments.py"};
myFixture.enableInspections(PyUnnecessaryBackslashInspection.class);
myFixture.configureByFiles(testFiles);
myFixture.checkHighlighting(true, false, true);
IntentionAction intentionAction = myFixture.getAvailableIntention(PyPsiBundle.message("QFIX.remove.unnecessary.backslash"));
assertNotNull(intentionAction);
myFixture.launchAction(intentionAction);
myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after"));
}
// PY-8788
public void testRenameShadowingBuiltins() {
final String fileName = "RenameShadowingBuiltins.py";
myFixture.configureByFile(fileName);
myFixture.enableInspections(PyShadowingBuiltinsInspection.class);
myFixture.checkHighlighting(true, false, true);
final IntentionAction intentionAction = myFixture.getAvailableIntention(PyBundle.message("QFIX.NAME.rename.element"));
assertNotNull(intentionAction);
myFixture.launchAction(intentionAction);
myFixture.checkResultByFile(graftBeforeExt(fileName, "_after"));
}
// PY-8788
public void testRenameFunctionShadowingBuiltins() {
final String fileName = "RenameFunctionShadowingBuiltins.py";
myFixture.configureByFile(fileName);
myFixture.enableInspections(PyShadowingBuiltinsInspection.class);
myFixture.checkHighlighting(true, false, true);
final IntentionAction intentionAction = myFixture.getAvailableIntention(PyBundle.message("QFIX.NAME.rename.element"));
assertNotNull(intentionAction);
myFixture.launchAction(intentionAction);
myFixture.checkResultByFile(graftBeforeExt(fileName, "_after"));
}
public void testIgnoreShadowingBuiltins() {
myFixture.configureByFile("IgnoreShadowingBuiltins.py");
myFixture.enableInspections(PyShadowingBuiltinsInspection.class);
final IntentionAction intentionAction = myFixture.getAvailableIntention("Ignore shadowed built-in name \"open\"");
assertNotNull(intentionAction);
myFixture.launchAction(intentionAction);
myFixture.checkHighlighting(true, false, true);
}
public void testImplementAbstractProperty() {
doInspectionTest("ImplementAbstractProperty.py", PyAbstractClassInspection.class, PyBundle.message("QFIX.NAME.implement.methods"),
true, true);
}
public void testImplementAbstractProperty1() {
doInspectionTest("ImplementAbstractProperty.py", PyAbstractClassInspection.class, PyBundle.message("QFIX.NAME.implement.methods"),
true, true);
}
public void testImplementAbstractOrder() {
doInspectionTest("ImplementAbstractOrder.py",
PyAbstractClassInspection.class,
PyBundle.message("QFIX.NAME.implement.methods"),
true,
true);
}
public void testRemovingUnderscoresInNumericLiterals() {
myFixture.configureByText(PythonFileType.INSTANCE, "1_0_0");
final IntentionAction action = myFixture.findSingleIntention(PyPsiBundle.message("QFIX.NAME.remove.underscores.in.numeric"));
myFixture.launchAction(action);
myFixture.checkResult("100");
}
// PY-20452
public void testRemoveRedundantEscapeInOnePartRegExp() {
myFixture.enableInspections(new RegExpRedundantEscapeInspection());
myFixture.configureByText(PythonFileType.INSTANCE, "import re\nre.compile(\"(?P<foo>((<caret>\\/(?P<bar>.+))?))\")");
final List<IntentionAction> quickFixes = myFixture.getAllQuickFixes();
assertEquals(1, quickFixes.size());
final IntentionAction removeRedundantEscapeFix = quickFixes.get(0);
assertEquals("Remove redundant escape", removeRedundantEscapeFix.getText());
myFixture.launchAction(removeRedundantEscapeFix);
myFixture.checkResult("import re\nre.compile(\"(?P<foo>((/(?P<bar>.+))?))\")");
}
// PY-20452
public void testRemoveRedundantEscapeInMultiPartRegExp() {
myFixture.enableInspections(new RegExpRedundantEscapeInspection());
myFixture.configureByText(PythonFileType.INSTANCE, """
import re
re.compile("(?P<foo>"
"((<caret>\\/(?P<bar>.+))?))")""");
final List<IntentionAction> quickFixes = myFixture.getAllQuickFixes();
assertEquals(1, quickFixes.size());
final IntentionAction removeRedundantEscapeFix = quickFixes.get(0);
assertEquals("Remove redundant escape", removeRedundantEscapeFix.getText());
myFixture.launchAction(removeRedundantEscapeFix);
myFixture.checkResult("""
import re
re.compile("(?P<foo>"
"((/(?P<bar>.+))?))")""");
}
// PY-8174
public void testChangeSignatureKeywordAndPositionalParameters() {
doInspectionTest(PyArgumentListInspection.class, "<html>Change the signature of f(x, foo)</html>", true, true);
}
// PY-8174
public void testChangeSignatureAddKeywordOnlyParameter() {
runWithLanguageLevel(
LanguageLevel.PYTHON34,
() -> doInspectionTest(PyArgumentListInspection.class, "<html>Change the signature of func(x, *args, foo)</html>", true, true)
);
}
// PY-8174
public void testChangeSignatureNewParametersNames() {
doInspectionTest(PyArgumentListInspection.class, "<html>Change the signature of func(i1)</html>", true, true);
}
// PY-53671
public void testChangeSignatureOfExportedBoundMethod() {
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
doMultiFilesInspectionTest(PyArgumentListInspection.class, "<html>Change the signature of method(self, a, b)</html>", "mod.py");
});
}
// PY-8174
public void testChangeSignatureParametersDefaultValues() {
doInspectionTest(PyArgumentListInspection.class, "<html>Change the signature of func()</html>", true, true);
}
public void testAddKwargsToNewMethodIncompatibleWithInit() {
doInspectionTest(PyInitNewSignatureInspection.class, "<html>Change the signature of __new__(cls)</html>", true, true);
}
public void testAddKwargsToIncompatibleOverridingMethod() {
doInspectionTest(PyMethodOverridingInspection.class, "<html>Change the signature of m(self)</html>", true, true);
}
// PY-30789
public void testSetImportedABCMetaAsMetaclassPy2() {
doInspectionTest("PyAbstractClassInspection/quickFix/SetImportedABCMetaAsMetaclassPy2/main.py",
PyAbstractClassInspection.class,
"Set '" + PyNames.ABC_META + "' as metaclass",
true,
true);
}
@Override
@NonNls
protected String getTestDataPath() {
return PythonTestUtil.getTestDataPath() + "/inspections/";
}
private void doInspectionTest(@NotNull Class<? extends LocalInspectionTool> inspectionClass,
@NotNull String quickFixName,
boolean applyFix,
boolean available) {
doInspectionTest(getTestName(false) + ".py", inspectionClass, quickFixName, applyFix, available);
}
protected void doInspectionTest(@TestDataFile @NonNls @NotNull String testFileName,
@NotNull Class<? extends LocalInspectionTool> inspectionClass,
@NonNls @NotNull String quickFixName,
boolean applyFix,
boolean available) {
doInspectionTest(new String[]{testFileName}, inspectionClass, quickFixName, applyFix, available);
}
/**
* Runs daemon passes and looks for given fix within infos.
*
* @param testFiles names of files to participate; first is used for inspection and then for check by "_after".
* @param inspectionClass what inspection to run
* @param quickFixName how the resulting fix should be named (the human-readable name users see)
* @param applyFix true if the fix needs to be applied
* @param available true if the fix should be available, false if it should be explicitly not available.
*/
protected void doInspectionTest(@NonNls String @NotNull [] testFiles,
@NotNull Class<? extends LocalInspectionTool> inspectionClass,
@NonNls @NotNull String quickFixName,
boolean applyFix,
boolean available) {
myFixture.enableInspections(inspectionClass);
myFixture.configureByFiles(testFiles);
myFixture.checkHighlighting(true, false, false);
final List<IntentionAction> intentionActions = myFixture.filterAvailableIntentions(quickFixName);
if (available) {
if (intentionActions.isEmpty()) {
final List<String> intentionNames = ContainerUtil.map(myFixture.getAvailableIntentions(), IntentionAction::getText);
throw new AssertionError("Quickfix starting with \"" + quickFixName + "\" is not available. " +
"Available intentions:\n" + StringUtil.join(intentionNames, "\n"));
}
if (intentionActions.size() > 1) {
throw new AssertionError("There are more than one quickfix with the name \"" + quickFixName + "\"");
}
if (applyFix) {
myFixture.launchAction(intentionActions.get(0));
NonBlockingReadActionImpl.waitForAsyncTaskCompletion();
myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after"), true);
}
}
else {
assertEmpty("Quick fix \"" + quickFixName + "\" should not be available", intentionActions);
}
}
private void doMultiFilesInspectionTest(@NotNull Class<? extends LocalInspectionTool> inspectionClass,
@NotNull String intentionStr,
@NotNull String modifiedFile) {
myFixture.enableInspections(inspectionClass);
myFixture.copyDirectoryToProject(getTestName(true), "");
myFixture.configureFromTempProjectFile(getTestName(true) + ".py");
myFixture.checkHighlighting(true, false, false);
final IntentionAction intentionAction = myFixture.findSingleIntention(intentionStr);
assertNotNull(intentionAction);
myFixture.launchAction(intentionAction);
final String expectedFile = getTestName(true) + "/" + graftBeforeExt(modifiedFile, "_after");
myFixture.checkResultByFile(modifiedFile, expectedFile, true);
}
// Turns "name.ext" to "name_insertion.ext"
@NonNls
private static String graftBeforeExt(String name, String insertion) {
int dotpos = name.indexOf('.');
if (dotpos < 0) dotpos = name.length();
return name.substring(0, dotpos) + insertion + name.substring(dotpos);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
// Copyright 2000-2025 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.jetbrains.python.Result
import com.jetbrains.python.packaging.pip.PypiPackageCache
import java.io.IOException
class TestPypiPackageCache : PypiPackageCache() {
var testPackages: Set<String> = emptySet()
override val packages: Set<String>
get() {
return testPackages
}
override suspend fun reloadCache(force: Boolean): Result<Unit, IOException> {
return Result.success(Unit)
}
}