diff --git a/python/src/com/jetbrains/python/inspections/quickfix/InstallAndImportPackageQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/InstallAndImportPackageQuickFix.kt index e274d348b979..a18a8fc68b7a 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/InstallAndImportPackageQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/quickfix/InstallAndImportPackageQuickFix.kt @@ -3,14 +3,17 @@ package com.jetbrains.python.inspections.quickfix import com.intellij.codeInspection.ProblemDescriptor -import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.command.writeCommandAction +import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.codeInsight.imports.AddImportHelper +import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls -internal class InstallAndImportPackageQuickFix( - override val packageName: String, +@ApiStatus.Internal +class InstallAndImportPackageQuickFix( + packageName: String, private val importAlias: String?, ) : InstallPackageQuickFix(packageName) { @@ -18,17 +21,10 @@ internal class InstallAndImportPackageQuickFix( override fun getFamilyName(): @Nls String = PyPsiBundle.message("QFIX.install.and.import.package") - override fun onSuccess(descriptor: ProblemDescriptor?) { - executeWriteCommandToAddImport(descriptor?.psiElement ?: return) - } - - private fun executeWriteCommandToAddImport(psiElement: PsiElement) { - WriteCommandAction.writeCommandAction(psiElement.project) - .withName(PyPsiBundle.message("INSP.package.requirements.add.import")) - .withGroupId(GROUP_ID) - .run { - addImportToFile(psiElement) - } + override suspend fun onSuccess(project: Project, descriptor: ProblemDescriptor) { + writeCommandAction(project, PyPsiBundle.message("INSP.package.requirements.add.import")) { + addImportToFile(descriptor.psiElement ?: return@writeCommandAction ) + } } private fun addImportToFile(element: PsiElement) = @@ -39,8 +35,4 @@ internal class InstallAndImportPackageQuickFix( AddImportHelper.ImportPriority.THIRD_PARTY, element ) - - companion object { - private const val GROUP_ID = "Add import" - } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt index 264babdb545c..dd9910dcb590 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt @@ -13,10 +13,12 @@ import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.statistics.PyPackagesUsageCollector +import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls // this is HighPriorityAction because we always want our fix to appear above lsp tools because our fix is much better -internal open class InstallPackageQuickFix(open val packageName: String) : LocalQuickFix, HighPriorityAction { +@ApiStatus.Internal +open class InstallPackageQuickFix(val packageName: String) : LocalQuickFix, HighPriorityAction { override fun getFamilyName(): @Nls String = PyBundle.message("python.unresolved.reference.inspection.install.package", packageName) override fun applyFix(project: Project, descriptor: ProblemDescriptor) { @@ -25,7 +27,7 @@ internal open class InstallPackageQuickFix(open val packageName: String) : Local ?: return PyPackageCoroutine.launch(project) { PythonPackageManagerUI.forSdk(project, sdk).installWithConfirmation(listOf(packageName)) ?: return@launch - onSuccess(descriptor) + onSuccess(project, descriptor) PyPackagesUsageCollector.installSingleEvent.log() } } @@ -34,7 +36,7 @@ internal open class InstallPackageQuickFix(open val packageName: String) : Local override fun availableInBatchMode(): Boolean = false - open fun onSuccess(descriptor: ProblemDescriptor?) {} + open suspend fun onSuccess(project: Project, descriptor: ProblemDescriptor) {} override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY } \ No newline at end of file diff --git a/python/testData/quickFixes/InstallPackageQuickFixTest/importPandas.py b/python/testData/quickFixes/InstallPackageQuickFixTest/importPandas.py new file mode 100644 index 000000000000..8d4cd312c083 --- /dev/null +++ b/python/testData/quickFixes/InstallPackageQuickFixTest/importPandas.py @@ -0,0 +1 @@ +pandas.array() diff --git a/python/testData/quickFixes/InstallPackageQuickFixTest/importPandasAsPd.py b/python/testData/quickFixes/InstallPackageQuickFixTest/importPandasAsPd.py new file mode 100644 index 000000000000..0788b100ebbe --- /dev/null +++ b/python/testData/quickFixes/InstallPackageQuickFixTest/importPandasAsPd.py @@ -0,0 +1 @@ +pd.array() diff --git a/python/testSrc/com/intellij/python/junit5Tests/unit/InstallPackageQuickFixTest.kt b/python/testSrc/com/intellij/python/junit5Tests/unit/InstallPackageQuickFixTest.kt new file mode 100644 index 000000000000..359a80ac058c --- /dev/null +++ b/python/testSrc/com/intellij/python/junit5Tests/unit/InstallPackageQuickFixTest.kt @@ -0,0 +1,57 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.junit5Tests.unit + +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.openapi.application.readAction +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.python.junit5Tests.framework.PyDefaultTestApplication +import com.intellij.python.junit5Tests.framework.metaInfo.TestClassInfo +import com.intellij.testFramework.TestDataPath +import com.intellij.testFramework.common.timeoutRunBlocking +import com.jetbrains.python.inspections.quickfix.InstallAndImportPackageQuickFix +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@PyDefaultTestApplication +@TestClassInfo +@TestDataPath($$"$CONTENT_ROOT/../testData/quickFixes/InstallPackageQuickFixTest") +internal class InstallPackageQuickFixTest(val project: Project) { + + @Test + fun testImportPandas(psiFile: PsiFile) = timeoutRunBlocking { + applyInstallAndImportQuickFix(psiFile, packageName = "pandas", importAlias = null) + assertEquals("import pandas\n\npandas.array()\n", psiFile.text) + } + + @Test + fun testImportPandasAsPd(psiFile: PsiFile) = timeoutRunBlocking { + applyInstallAndImportQuickFix(psiFile, packageName = "pandas", importAlias = "pd") + assertEquals("import pandas as pd\n\npd.array()\n", psiFile.text) + } + + /** + * The test currently calls onSuccess directly (bypassing applyFix / real package install), + * because I didn't find an easy way to run the complete inspection. + * The [com.jetbrains.python.packaging.pip.PypiPackageCache] cache is not initialized in tests + * also [com.jetbrains.python.inspections.quickfix.InstallPackageQuickFix] is not available in batch mode and the inspection filters it out + */ + private suspend fun applyInstallAndImportQuickFix(psiFile: PsiFile, packageName: String, importAlias: String?) { + val descriptor = readAction { + val element = psiFile.findElementAt(0)!! + InspectionManager.getInstance(project) + .createProblemDescriptor( + element, + "", + null as LocalQuickFix?, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + true + ) + } + + val quickFix = InstallAndImportPackageQuickFix(packageName, importAlias) + quickFix.onSuccess(project, descriptor) + } +}