From f5d815d1e4a8bdc4f5c62ff78fe5febe257e5a7d Mon Sep 17 00:00:00 2001 From: "Nikita.Ashihmin" Date: Thu, 31 Jul 2025 13:28:03 +0400 Subject: [PATCH] PY-78749 Packages: refactor install utils, migrate methods to PPMUI Signed-off-by: Nikita.Ashihmin GitOrigin-RevId: 69eaeb90dd326f7e5cb07d67f615e939980d536b --- .../src/com/jetbrains/python/Result.kt | 5 +- .../intellij.python.community.impl.xml | 1 + .../quickfix/InstallAllPackagesQuickFix.kt | 19 +- .../quickfix/InstallPackageQuickFix.kt | 33 +- .../quickfix/PyInstallRequirementsFix.kt | 106 -- .../PyUnresolvedReferencesInspection.kt | 23 +- .../python/packaging/PyPackageInstallUtils.kt | 115 -- .../management/PythonPackageManagerExt.kt | 5 + .../management/PythonRepositoryManager.kt | 5 + .../PyPackageManagerUiConfirmationHelpers.kt | 72 + .../management/ui/PythonPackageManagerUI.kt | 28 +- .../ui/PythonPackageManagerUiExtensions.kt | 3 +- .../packaging/pip/PipPackageManagerEngine.kt | 13 +- .../python/packaging/pip/PypiPackageCache.kt | 7 +- .../python/packaging/utils/PyProxyUtils.kt | 16 + .../InstallAllRequirementsQuickFix.kt | 16 +- .../quickfixes/InstallRequirementQuickFix.kt | 4 +- .../run/filter/PythonInstallPackageFilter.kt | 15 +- .../com/jetbrains/python/PyQuickFixTest.java | 867 ---------- .../com/jetbrains/python/PyQuickFixTest.kt | 1402 +++++++++++++++++ .../management/TestPypiPackageCache.kt | 18 + 21 files changed, 1593 insertions(+), 1180 deletions(-) delete mode 100644 python/src/com/jetbrains/python/inspections/quickfix/PyInstallRequirementsFix.kt delete mode 100644 python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt create mode 100644 python/src/com/jetbrains/python/packaging/management/ui/PyPackageManagerUiConfirmationHelpers.kt create mode 100644 python/src/com/jetbrains/python/packaging/utils/PyProxyUtils.kt delete mode 100644 python/testSrc/com/jetbrains/python/PyQuickFixTest.java create mode 100644 python/testSrc/com/jetbrains/python/PyQuickFixTest.kt create mode 100644 python/testSrc/com/jetbrains/python/packaging/management/TestPypiPackageCache.kt diff --git a/python/openapi/src/com/jetbrains/python/Result.kt b/python/openapi/src/com/jetbrains/python/Result.kt index 044f362e64e3..c69c4abaa53b 100644 --- a/python/openapi/src/com/jetbrains/python/Result.kt +++ b/python/openapi/src/com/jetbrains/python/Result.kt @@ -114,12 +114,15 @@ sealed class Result { fun success(value: S): Success = Success(value) fun failure(error: E): Failure = Failure(error) fun localizedError(message: @Nls String): Failure = failure(MessageError(message)) + + @ApiStatus.Internal + inline fun runCatching(body: () -> T): PyResult = pyRunCatching(body) } } @Suppress("UsagesOfObsoleteApi") @ApiStatus.Internal -fun pyRunCatching(body: () -> T): PyResult { +inline fun pyRunCatching(body: () -> T): PyResult { return try { PyResult.success(body()) } diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml index 0ac9a388b11d..7f0195448b8a 100644 --- a/python/pluginResources/intellij.python.community.impl.xml +++ b/python/pluginResources/intellij.python.community.impl.xml @@ -134,6 +134,7 @@ testServiceImplementation="com.jetbrains.python.codeInsight.completion.DummyRemoteFilePathRetrievalService"/> + diff --git a/python/src/com/jetbrains/python/inspections/quickfix/InstallAllPackagesQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/InstallAllPackagesQuickFix.kt index 8cb03bfc2448..e238dda9062d 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/InstallAllPackagesQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/quickfix/InstallAllPackagesQuickFix.kt @@ -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) : 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") diff --git a/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt index d27a95c7b701..55de79f99161 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt @@ -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) { - 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" - } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/inspections/quickfix/PyInstallRequirementsFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/PyInstallRequirementsFix.kt deleted file mode 100644 index 12201bbc458c..000000000000 --- a/python/src/com/jetbrains/python/inspections/quickfix/PyInstallRequirementsFix.kt +++ /dev/null @@ -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, - private val installOptions: List = 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: - *
    - *
  • 0: The user selected the "Configure" option.
  • - *
  • 1: The user selected the "Install Anyway" option.
  • - *
  • 2: The user selected the "Cancel" option.
  • - *
- */ - 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 - } -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt index edf8e7536679..f027244a85ce 100644 --- a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt +++ b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt @@ -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 } diff --git a/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt b/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt deleted file mode 100644 index a3b8b44b15e8..000000000000 --- a/python/src/com/jetbrains/python/packaging/PyPackageInstallUtils.kt +++ /dev/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, project: Project): Set { - 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) - } - } - } -} - - diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt index 9a776eaabbf7..5e56321edfc1 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerExt.kt @@ -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, diff --git a/python/src/com/jetbrains/python/packaging/management/PythonRepositoryManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonRepositoryManager.kt index 5f96a1d3fb63..243f06dada45 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonRepositoryManager.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonRepositoryManager.kt @@ -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, diff --git a/python/src/com/jetbrains/python/packaging/management/ui/PyPackageManagerUiConfirmationHelpers.kt b/python/src/com/jetbrains/python/packaging/management/ui/PyPackageManagerUiConfirmationHelpers.kt new file mode 100644 index 000000000000..cb5874cf967f --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/management/ui/PyPackageManagerUiConfirmationHelpers.kt @@ -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, project: Project): List { + 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) + } + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt index d255e98f7a15..65c3239fb72a 100644 --- a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt +++ b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUI.kt @@ -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): List? { + val requirements = packages.map { pyRequirement(it) } + return installPyRequirementsWithConfirmation(requirements) + } + + + suspend fun installPyRequirementsWithConfirmation(packages: List): List? { + 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 executeCommand( progressTitle: @Nls String, operation: suspend (() -> PyResult?), ): T? = PythonPackageManagerUIHelpers.runPackagingOperationMaybeBackground(manager.project, sink, progressTitle) { - operation() + operation() } companion object { diff --git a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUiExtensions.kt b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUiExtensions.kt index 3f7e46bb599c..1183fe716044 100644 --- a/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUiExtensions.kt +++ b/python/src/com/jetbrains/python/packaging/management/ui/PythonPackageManagerUiExtensions.kt @@ -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) diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerEngine.kt b/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerEngine.kt index 33eb74113115..5967ebdf785c 100644 --- a/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerEngine.kt +++ b/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerEngine.kt @@ -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> { when (installRequest) { diff --git a/python/src/com/jetbrains/python/packaging/pip/PypiPackageCache.kt b/python/src/com/jetbrains/python/packaging/pip/PypiPackageCache.kt index 7bbac34b2559..8799b9a3f254 100644 --- a/python/src/com/jetbrains/python/packaging/pip/PypiPackageCache.kt +++ b/python/src/com/jetbrains/python/packaging/pip/PypiPackageCache.kt @@ -37,8 +37,7 @@ private val LOG = logger() private val ALPHABET_REGEX = Regex("[-a-z0-9]+") @ApiStatus.Internal -@Service -class PypiPackageCache : PythonPackageCache { +open class PypiPackageCache : PythonPackageCache { override val packages: Set get() = cache @@ -63,7 +62,7 @@ class PypiPackageCache : PythonPackageCache { val filePath: Path = Paths.get(PathManager.getSystemPath(), "python_packages", "packages_v2.json") @CheckReturnValue - suspend fun reloadCache(force: Boolean = false): Result { + open suspend fun reloadCache(force: Boolean = false): Result { lock.withLock { if ((cache.isNotEmpty() && !force) || loadInProgress) { return Result.success(Unit) @@ -93,7 +92,7 @@ class PypiPackageCache : PythonPackageCache { return@withContext false } - var packageList = emptySet() + var packageList: Set try { val type = object : TypeToken>() {}.type packageList = Files.newBufferedReader(filePath, StandardCharsets.UTF_8) diff --git a/python/src/com/jetbrains/python/packaging/utils/PyProxyUtils.kt b/python/src/com/jetbrains/python/packaging/utils/PyProxyUtils.kt new file mode 100644 index 000000000000..ce12730cbc44 --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/utils/PyProxyUtils.kt @@ -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 + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt index ad1f018a40a7..7a33c19e0152 100644 --- a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt +++ b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallAllRequirementsQuickFix.kt @@ -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) : LocalQuickFix { override fun getFamilyName(): String { @@ -19,17 +17,9 @@ internal class InstallAllRequirementsQuickFix(val requirements: List() - ) + PythonPackageManagerUI.forSdk(project, sdk).installPyRequirementsWithConfirmation(requirements) } } diff --git a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallRequirementQuickFix.kt b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallRequirementQuickFix.kt index da17a566ef2c..28833dd1083e 100644 --- a/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallRequirementQuickFix.kt +++ b/python/src/com/jetbrains/python/requirements/inspections/quickfixes/InstallRequirementQuickFix.kt @@ -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)) } } diff --git a/python/src/com/jetbrains/python/run/filter/PythonInstallPackageFilter.kt b/python/src/com/jetbrains/python/run/filter/PythonInstallPackageFilter.kt index da7b21e9925c..a143d9266dce 100644 --- a/python/src/com/jetbrains/python/run/filter/PythonInstallPackageFilter.kt +++ b/python/src/com/jetbrains/python/run/filter/PythonInstallPackageFilter.kt @@ -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, diff --git a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java deleted file mode 100644 index 3170a0713831..000000000000 --- a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java +++ /dev/null @@ -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.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((\\/(?P.+))?))\")"); - - final List 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((/(?P.+))?))\")"); - } - - // PY-20452 - public void testRemoveRedundantEscapeInMultiPartRegExp() { - myFixture.enableInspections(new RegExpRedundantEscapeInspection()); - myFixture.configureByText(PythonFileType.INSTANCE, """ - import re - re.compile("(?P" - "((\\/(?P.+))?))")"""); - - final List 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" - "((/(?P.+))?))")"""); - } - - // PY-8174 - public void testChangeSignatureKeywordAndPositionalParameters() { - doInspectionTest(PyArgumentListInspection.class, "Change the signature of f(x, foo)", true, true); - } - - // PY-8174 - public void testChangeSignatureAddKeywordOnlyParameter() { - runWithLanguageLevel( - LanguageLevel.PYTHON34, - () -> doInspectionTest(PyArgumentListInspection.class, "Change the signature of func(x, *args, foo)", true, true) - ); - } - - // PY-8174 - public void testChangeSignatureNewParametersNames() { - doInspectionTest(PyArgumentListInspection.class, "Change the signature of func(i1)", true, true); - } - - // PY-53671 - public void testChangeSignatureOfExportedBoundMethod() { - runWithLanguageLevel(LanguageLevel.getLatest(), () -> { - doMultiFilesInspectionTest(PyArgumentListInspection.class, "Change the signature of method(self, a, b)", "mod.py"); - }); - } - - // PY-8174 - public void testChangeSignatureParametersDefaultValues() { - doInspectionTest(PyArgumentListInspection.class, "Change the signature of func()", true, true); - } - - public void testAddKwargsToNewMethodIncompatibleWithInit() { - doInspectionTest(PyInitNewSignatureInspection.class, "Change the signature of __new__(cls)", true, true); - } - - public void testAddKwargsToIncompatibleOverridingMethod() { - doInspectionTest(PyMethodOverridingInspection.class, "Change the signature of m(self)", 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 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 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 inspectionClass, - @NonNls @NotNull String quickFixName, - boolean applyFix, - boolean available) { - myFixture.enableInspections(inspectionClass); - myFixture.configureByFiles(testFiles); - myFixture.checkHighlighting(true, false, false); - final List intentionActions = myFixture.filterAvailableIntentions(quickFixName); - if (available) { - if (intentionActions.isEmpty()) { - final List 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 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); - } -} diff --git a/python/testSrc/com/jetbrains/python/PyQuickFixTest.kt b/python/testSrc/com/jetbrains/python/PyQuickFixTest.kt new file mode 100644 index 000000000000..cdc959671aa1 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/PyQuickFixTest.kt @@ -0,0 +1,1402 @@ +// 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 + +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ex.InspectionProfileImpl +import com.intellij.openapi.application.ApplicationManager +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.testFramework.replaceService +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.management.TestPypiPackageCache +import com.jetbrains.python.packaging.pip.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 + +@TestDataPath("\$CONTENT_ROOT/../testData/inspections/") +class PyQuickFixTest : PyTestCase() { + override fun getProjectDescriptor(): LightProjectDescriptor? = ourPy2Descriptor + + @Throws(Exception::class) + override fun setUp() { + super.setUp() + InspectionProfileImpl.INIT_INSPECTIONS = true + myFixture.setCaresAboutInjection(false) + PyRenameElementQuickFixTest.registerTestNameSuggestionProvider(testRootDisposable) + } + + @Throws(Exception::class) + override fun tearDown() { + InspectionProfileImpl.INIT_INSPECTIONS = false + super.tearDown() + } + + fun testAddImport() { + doInspectionTest( + arrayOf("AddImport.py", "ImportTarget.py"), + PyUnresolvedReferencesInspection::class.java, + "Import 'ImportTarget'", + true, + true + ) + } + + fun testAddImportDoc() { + doInspectionTest( + arrayOf("AddImportDoc.py", "ImportTarget.py"), + PyUnresolvedReferencesInspection::class.java, + "Import 'ImportTarget'", + true, + true + ) + } + + // PY-728 + fun testAddImportDocComment() { + doInspectionTest( + arrayOf("AddImportDocComment.py", "ImportTarget.py"), + PyUnresolvedReferencesInspection::class.java, + "Import 'ImportTarget'", + true, + true + ) + } + + // PY-42307 + fun testInstallAndImportPackageByNameAlias() { + val packageCache = TestPypiPackageCache() + packageCache.testPackages = setOf("pandas") + ApplicationManager.getApplication().replaceService(PypiPackageCache::class.java, + packageCache, + testRootDisposable) + myFixture.enableInspections(PyUnresolvedReferencesInspection::class.java) + myFixture.configureByText(PythonFileType.INSTANCE, "pd.array()") + myFixture.findSingleIntention("Import 'turtle.pd'") // standard library + myFixture.findSingleIntention("Install and import package 'pandas'") // 'pd' is a common import alias for 'pandas' from PyPI + } + + fun testImportFromModule() { + doInspectionTest( + arrayOf("importFromModule/foo/bar.py", "importFromModule/foo/baz.py", "importFromModule/foo/__init__.py"), + PyUnresolvedReferencesInspection::class.java, + "Import 'importFromModule.foo.baz'", + true, + true + ) + } + + // PY-14365 + fun testObjectBaseIsNotShownInAutoImportQuickfix() { + myFixture.copyDirectoryToProject("objectBaseIsNotShownInAutoImportQuickfix", "") + myFixture.configureByFile("main.py") + myFixture.enableInspections(PyUnresolvedReferencesInspection::class.java) + val intention = myFixture.findSingleIntention("Import") + assertNotNull(intention) + assertEquals("Import 'module.MyOldStyleClass'", intention.text) + } + + // PY-6302 + fun testImportFromModuleStar() { + myFixture.enableInspections(PyUnresolvedReferencesInspection::class.java) + myFixture.copyDirectoryToProject("importFromModuleStar", "") + myFixture.configureFromTempProjectFile("source.py") + myFixture.checkHighlighting(true, false, false) + val intentionAction = myFixture.findSingleIntention("Import 'target.xyzzy'") + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkResultByFile("importFromModuleStar/source_after.py") + } + + fun testQualifyByImport() { + val settings = PyCodeInsightSettings.getInstance() + val oldPreferFrom = settings.PREFER_FROM_IMPORT + settings.PREFER_FROM_IMPORT = false + try { + doInspectionTest( + arrayOf("QualifyByImport.py", "QualifyByImportFoo.py"), + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("ACT.qualify.with.module"), + true, + true + ) + } + finally { + settings.PREFER_FROM_IMPORT = oldPreferFrom + } + } + + fun testAddToImportFromList() { + doInspectionTest( + arrayOf("AddToImportFromList.py", "AddToImportFromFoo.py"), + PyUnresolvedReferencesInspection::class.java, + "Import 'add_to_import_test_unique_name from AddToImportFromFoo'", + true, + true + ) + } + + fun testAddSelf() { + doInspectionTest( + PyMethodParametersInspection::class.java, + PyPsiBundle.message("QFIX.add.parameter.self", "self"), + true, + true + ) + } + + fun testReplacePrint() { + doInspectionTest( + PyCompatibilityInspection::class.java, + PyPsiBundle.message("QFIX.statement.effect"), + true, + true + ) + } + + // PY-22045 + fun testBatchReplacePrintInsertsFutureImportOnlyOnce() { + doInspectionTest( + PyCompatibilityInspection::class.java, + "Fix all 'Code is incompatible with specific Python versions' problems in file", + true, + true + ) + } + + // PY-4556 + fun testAddSelfFunction() { + doInspectionTest( + "AddSelfFunction.py", + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.unresolved.reference", "get_a", "self"), + true, + true + ) + } + + // PY-9721 + fun testAddSelfToClassmethod() { + doInspectionTest( + "AddSelfToClassmethod.py", + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.unresolved.reference", "foo", "cls"), + true, + true + ) + } + + fun testAddCls() { + doInspectionTest( + PyMethodParametersInspection::class.java, + PyPsiBundle.message("QFIX.add.parameter.self", "cls"), + true, + true + ) + } + + fun testRenameToSelf() { + doInspectionTest( + PyMethodParametersInspection::class.java, + PyPsiBundle.message("QFIX.rename.parameter", "self"), + true, + true + ) + } + + fun testRemoveTrailingSemicolon() { + doInspectionTest( + PyTrailingSemicolonInspection::class.java, + PyPsiBundle.message("QFIX.remove.trailing.semicolon"), + true, + true + ) + } + + fun testDictCreation() { + doInspectionTest( + PyDictCreationInspection::class.java, + PyPsiBundle.message("QFIX.dict.creation"), + true, + true + ) + } + + // PY-6283 + fun testDictCreationTuple() { + doInspectionTest( + PyDictCreationInspection::class.java, + PyPsiBundle.message("QFIX.dict.creation"), + true, + true + ) + } + + // PY-7318 + fun testDictCreationDuplicate() { + doInspectionTest( + PyDictCreationInspection::class.java, + PyPsiBundle.message("QFIX.dict.creation"), + true, + true + ) + } + + // PY-40177 + fun testDictCreationWithDoubleStars() { + runWithLanguageLevel(LanguageLevel.getLatest()) { + doInspectionTest( + PyDictCreationInspection::class.java, + PyPsiBundle.message("QFIX.dict.creation"), + true, + true + ) + } + } + + fun testTransformClassicClass() { + doInspectionTest( + PyClassicStyleClassInspection::class.java, + PyPsiBundle.message("QFIX.classic.class.transform"), + true, + true + ) + } + + fun testAddGlobalStatement() { + doInspectionTest( + PyUnboundLocalVariableInspection::class.java, + PyPsiBundle.message("QFIX.add.global"), + true, + true + ) + } + + fun testAddGlobalExistingStatement() { + doInspectionTest( + PyUnboundLocalVariableInspection::class.java, + PyPsiBundle.message("QFIX.add.global"), + true, + true + ) + } + + fun testSimplifyBooleanCheck() { + doInspectionTest( + PySimplifyBooleanCheckInspection::class.java, + PyPsiBundle.message("QFIX.simplify.boolean.expression", "b"), + true, + true + ) + } + + fun testMoveFromFutureImport() { + doInspectionTest( + PyFromFutureImportInspection::class.java, + PyPsiBundle.message("QFIX.move.from.future.import"), + true, + true + ) + } + + // PY-10080 + fun testMoveFromFutureImportDocString() { + doInspectionTest( + PyFromFutureImportInspection::class.java, + PyPsiBundle.message("QFIX.move.from.future.import"), + true, + true + ) + } + + // PY-23475 + fun testMoveFromFutureImportAboveModuleLevelDunder() { + doInspectionTest( + PyFromFutureImportInspection::class.java, + PyPsiBundle.message("QFIX.move.from.future.import"), + true, + true + ) + } + + fun testComparisonWithNone() { + doInspectionTest( + PyComparisonWithNoneInspection::class.java, + PyPsiBundle.message("QFIX.replace.equality"), + true, + true + ) + } + + fun testAddClassFix() { + doInspectionTest( + "AddClass.py", + PyUnresolvedReferencesInspection::class.java, + "Create class 'Xyzzy'", + true, + true + ) + } + + // PY-42389 + fun testAddClassFixPython3() { + runWithLanguageLevel(LanguageLevel.getLatest()) { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + "Create class 'Xyzzy'", + true, + true + ) + } + } + + // PY-21204 + fun testAddClassFromTypeComment() { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + "Create class 'MyClass'", + true, + true + ) + } + + // PY-21204 + fun testAddClassFromFString() { + runWithLanguageLevel(LanguageLevel.PYTHON36) { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + "Create class 'MyClass'", + true, + true + ) + } + } + + // PY-33802 + fun testAddClassToImportedModule() { + doMultiFilesInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.create.class.in.module", "Clzz", "mod.py"), + "mod.py" + ) + } + + // PY-33802 + fun testAddClassToImportedPackage() { + doMultiFilesInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.create.class.in.module", "Clzz", "__init__.py"), + "pkg/__init__.py" + ) + } + + // PY-33802 + fun testAddClassToModuleInFromImport() { + doMultiFilesInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.create.class.in.module", "Clzz", "mod.py"), + "mod.py" + ) + } + + // PY-33802 + fun testAddClassToPackageInFromImport() { + doMultiFilesInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.create.class.in.module", "Clzz", "__init__.py"), + "mypack/__init__.py" + ) + } + + // PY-21204 + fun testAddFunctionFromFString() { + runWithLanguageLevel(LanguageLevel.PYTHON36) { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.NAME.unresolved.reference.create.function", "my_function"), + true, + true + ) + } + } + + // PY-1465 + fun testAddFunctionToModuleInImport() { + doMultiFilesInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.create.function.in.module", "func", "mod.py"), + "mod.py" + ) + } + + // PY-34710 + fun testAddFunctionToModuleInFromImport() { + doMultiFilesInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.create.function.in.module", "foo", "mod.py"), + "mod.py" + ) + } + + // PY-34710 + fun testAddFunctionToPackageInFromImport() { + doMultiFilesInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.create.function.in.module", "foo", "__init__.py"), + "mypack/__init__.py" + ) + } + + // PY-1470 + fun testRedundantParentheses() { + val testFiles = arrayOf("RedundantParentheses.py") + myFixture.enableInspections(PyRedundantParenthesesInspection::class.java) + myFixture.configureByFiles(*testFiles) + myFixture.checkHighlighting(true, false, true) + val intentionAction = myFixture.findSingleIntention(PyPsiBundle.message("QFIX.redundant.parentheses")) + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after")) + } + + // PY-3095 + fun testRedundantParenthesesBoolean() { + doInspectionTest( + PyRedundantParenthesesInspection::class.java, + PyPsiBundle.message("QFIX.redundant.parentheses"), + true, + true + ) + } + + // PY-3239 + fun testRedundantParenthesesMore() { + doInspectionTest( + PyRedundantParenthesesInspection::class.java, + PyPsiBundle.message("QFIX.redundant.parentheses"), + true, + true + ) + } + + // PY-12679 + fun testRedundantParenthesesParenthesizedExpression() { + doInspectionTest( + PyRedundantParenthesesInspection::class.java, + PyPsiBundle.message("QFIX.redundant.parentheses"), + true, + true + ) + } + + fun testRedundantParenthesesMultipleParentheses() { + doInspectionTest( + PyRedundantParenthesesInspection::class.java, + PyPsiBundle.message("QFIX.redundant.parentheses"), + true, + true + ) + } + + // PY-15506 + fun testEmptyListOfBaseClasses() { + doInspectionTest( + PyRedundantParenthesesInspection::class.java, + PyPsiBundle.message("QFIX.redundant.parentheses"), + true, + true + ) + } + + // PY-18203 + fun testRedundantParenthesesInTuples() { + doInspectionTest( + PyRedundantParenthesesInspection::class.java, + PyPsiBundle.message("QFIX.redundant.parentheses"), + true, + true + ) + } + + // PY-1020 + fun testChainedComparisons() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + PyPsiBundle.message("QFIX.chained.comparison"), + true, + true + ) + } + + // PY-3126 + fun testChainedComparison1() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + PyPsiBundle.message("QFIX.chained.comparison"), + true, + true + ) + } + + // PY-3126 + fun testChainedComparison2() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + PyPsiBundle.message("QFIX.chained.comparison"), + true, + true + ) + } + + // PY-3126 + fun testChainedComparison3() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + PyPsiBundle.message("QFIX.chained.comparison"), + true, + true + ) + } + + // PY-5623 + fun testChainedComparison4() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + PyPsiBundle.message("QFIX.chained.comparison"), + true, + true + ) + } + + // PY-6467 + fun testChainedComparison5() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + PyPsiBundle.message("QFIX.chained.comparison"), + true, + true + ) + } + + // PY-20004 + fun testChainedComparison7() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + PyPsiBundle.message("QFIX.chained.comparison"), + true, + true + ) + } + + // PY-14002 + fun testChainedComparisonWithCommonBinaryExpression() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + PyPsiBundle.message("QFIX.chained.comparison"), + true, + true + ) + } + + // PY-19583 + fun testChainedComparison6() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + "Simplify chained comparison", + true, + true + ) + } + + // PY-24942 + fun testChainedComparison8() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + "Simplify chained comparison", + true, + true + ) + } + + // PY-29121 + fun testChainedComparison9() { + doInspectionTest( + PyChainedComparisonsInspection::class.java, + "Simplify chained comparison", + true, + true + ) + } + + // PY-1362, PY-2585 + fun testStatementEffect() { + doInspectionTest( + PyStatementEffectInspection::class.java, + PyPsiBundle.message("QFIX.statement.effect"), + true, + true + ) + } + + // PY-1265 + fun testStatementEffectIntroduceVariable() { + doInspectionTest( + PyStatementEffectInspection::class.java, + PyPsiBundle.message("QFIX.introduce.variable"), + true, + true + ) + } + + // PY-2092 + fun testUnresolvedRefCreateFunction() { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.NAME.unresolved.reference.create.function", "ref"), + true, + true + ) + } + + fun testUnresolvedRefCreateAsyncFunction() { + runWithLanguageLevel(LanguageLevel.getLatest()) { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.NAME.unresolved.reference.create.function", "ref"), + true, + true + ) + } + } + + fun testUnresolvedRefNoCreateFunction() { + myFixture.enableInspections(PyUnresolvedReferencesInspection::class.java) + myFixture.configureByFile("UnresolvedRefNoCreateFunction.py") + myFixture.checkHighlighting(true, false, false) + val intentionAction = myFixture.getAvailableIntention( + PyPsiBundle.message("QFIX.NAME.unresolved.reference.create.function", "ref") + ) + assertNull(intentionAction) + } + + fun testReplaceNotEqOperator() { + doInspectionTest( + PyCompatibilityInspection::class.java, + PyPsiBundle.message("INTN.replace.noteq.operator"), + true, + true + ) + } + + fun testListCreation() { + doInspectionTest( + PyListCreationInspection::class.java, + PyPsiBundle.message("QFIX.list.creation"), + true, + true + ) + } + + // PY-16194 + fun testListCreationOnlyConsecutiveAppends() { + doInspectionTest( + PyListCreationInspection::class.java, + PyPsiBundle.message("QFIX.list.creation"), + true, + true + ) + } + + // PY-1445 + fun testConvertSingleQuotedDocstring() { + indentOptions.INDENT_SIZE = 2 + doInspectionTest( + PySingleQuotedDocstringInspection::class.java, + PyPsiBundle.message("QFIX.convert.single.quoted.docstring"), + true, + true + ) + } + + // PY-8926 + fun testConvertSingleQuotedDocstringEscape() { + indentOptions.INDENT_SIZE = 2 + doInspectionTest( + PySingleQuotedDocstringInspection::class.java, + PyPsiBundle.message("QFIX.convert.single.quoted.docstring"), + true, + true + ) + } + + // PY-3127 + fun testDefaultArgument() { + doInspectionTest( + PyDefaultArgumentInspection::class.java, + PyPsiBundle.message("QFIX.default.argument"), + true, + true + ) + } + + fun testDefaultArgumentEmptyList() { + doInspectionTest( + PyDefaultArgumentInspection::class.java, + PyPsiBundle.message("QFIX.default.argument"), + true, + true + ) + } + + // PY-17392 + fun testDefaultArgumentCommentsInsideParameters() { + doInspectionTest( + PyDefaultArgumentInspection::class.java, + PyPsiBundle.message("QFIX.default.argument"), + true, + true + ) + } + + // PY-3125 + fun testArgumentEqualDefault() { + doInspectionTest( + PyArgumentEqualDefaultInspection::class.java, + PyPsiBundle.message("QFIX.remove.argument.equal.default"), + true, + true + ) + } + + // PY-3315 + fun testAddCallSuper() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-4017 + fun testAddCallSuper1() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-8654 + fun testAddCallSuperPass() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-15867 + fun testAddCallSuperOptionalAndRequiredParamsNameCollision() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-15927 + fun testAddCallSuperConflictingTupleParam() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-16036 + fun testAddCallSuperSelfNamePreserved() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-16420 + fun testAddCallSuperRepeatedOptionalParamsPassedToSuperConstructor() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-16420 + fun testAddCallSuperRepeatedOptionalTupleParamsPassedToSuperConstructor() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-16289 + fun testAddCallSuperCommentAfterColonPreserved() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-16348 + fun testAddCallSuperCommentsInFunctionBodyPreserved() { + doInspectionTest( + PyMissingConstructorInspection::class.java, + PyPsiBundle.message("QFIX.add.super"), + true, + true + ) + } + + // PY-491, PY-13297 + fun testAddEncoding() { + doInspectionTest( + PyMandatoryEncodingInspection::class.java, + PyPsiBundle.message("QFIX.add.encoding"), + true, + true + ) + } + + // PY-13297 + fun testAddEncodingAtLastLine() { + doInspectionTest( + PyMandatoryEncodingInspection::class.java, + PyPsiBundle.message("QFIX.add.encoding"), + true, + true + ) + } + + // PY-3348 + fun testRemoveDecorator() { + doInspectionTest( + PyDecoratorInspection::class.java, + PyPsiBundle.message("QFIX.remove.decorator"), + true, + true + ) + } + + fun testAddParameter() { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.unresolved.reference.add.param", "test"), + true, + true + ) + } + + // PY-6595 + fun testRenameUnresolvedReference() { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.rename.unresolved.reference"), + true, + true + ) + } + + // PY-3120 + fun testSetFunctionToLiteral() { + runWithLanguageLevel(LanguageLevel.PYTHON27) { + doInspectionTest( + PySetFunctionToLiteralInspection::class.java, + PyPsiBundle.message("QFIX.replace.function.set.with.literal"), + true, + true + ) + } + } + + // PY-3394 + fun testDocstringParams() { + indentOptions.INDENT_SIZE = 2 + runWithDocStringFormat(DocStringFormat.REST) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.add.parameter", "b"), + true, + true + ) + } + } + + fun testDocstringParams1() { + indentOptions.INDENT_SIZE = 2 + runWithDocStringFormat(DocStringFormat.REST) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "c"), + true, + true + ) + } + } + + // PY-4964 + fun testDocstringParams2() { + runWithDocStringFormat(DocStringFormat.REST) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.add.parameter", "ham"), + true, + true + ) + } + } + + // PY-9795 + fun testGoogleDocStringAddParam() { + runWithDocStringFormat(DocStringFormat.GOOGLE) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.add.parameter", "b"), + true, + true + ) + } + } + + // PY-9795 + fun testGoogleDocStringRemoveParam() { + runWithDocStringFormat(DocStringFormat.GOOGLE) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "c"), + true, + true + ) + } + } + + // PY-9795 + fun testGoogleDocStringRemoveParamWithSection() { + runWithDocStringFormat(DocStringFormat.GOOGLE) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "c"), + true, + true + ) + } + } + + // PY-16761 + fun testGoogleDocStringRemovePositionalVararg() { + runWithDocStringFormat(DocStringFormat.GOOGLE) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "args"), + true, + true + ) + } + } + + // PY-16761 + fun testGoogleDocStringRemoveKeywordVararg() { + runWithDocStringFormat(DocStringFormat.GOOGLE) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "kwargs"), + true, + true + ) + } + } + + // PY-16908 + fun testNumpyDocStringRemoveFirstOfCombinedParams() { + runWithDocStringFormat(DocStringFormat.NUMPY) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "x"), + true, + true + ) + } + } + + // PY-16908 + fun testNumpyDocStringRemoveMidOfCombinedParams() { + runWithDocStringFormat(DocStringFormat.NUMPY) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "y"), + true, + true + ) + } + } + + // PY-16908 + fun testNumpyDocStringRemoveLastOfCombinedParams() { + runWithDocStringFormat(DocStringFormat.NUMPY) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "z"), + true, + true + ) + } + } + + // PY-16908 + fun testNumpyDocStringRemoveCombinedVarargParam() { + runWithDocStringFormat(DocStringFormat.NUMPY) { + doInspectionTest( + PyIncorrectDocstringInspection::class.java, + PyPsiBundle.message("QFIX.docstring.remove.parameter", "args"), + true, + true + ) + } + } + + fun testUnnecessaryBackslash() { + val testFiles = arrayOf("UnnecessaryBackslash.py") + myFixture.enableInspections(PyUnnecessaryBackslashInspection::class.java) + myFixture.configureByFiles(*testFiles) + myFixture.checkHighlighting(true, false, true) + val intentionAction = myFixture.getAvailableIntention(PyPsiBundle.message("QFIX.remove.unnecessary.backslash"))!! + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after")) + } + + // PY-3051 + fun testUnresolvedRefTrueFalse() { + doInspectionTest( + PyUnresolvedReferencesInspection::class.java, + PyPsiBundle.message("QFIX.replace.with.true.or.false", "True"), + true, + true + ) + } + + fun testUnnecessaryBackslashInArgumentList() { + val testFiles = arrayOf("UnnecessaryBackslashInArguments.py") + myFixture.enableInspections(PyUnnecessaryBackslashInspection::class.java) + myFixture.configureByFiles(*testFiles) + myFixture.checkHighlighting(true, false, true) + val intentionAction = myFixture.getAvailableIntention(PyPsiBundle.message("QFIX.remove.unnecessary.backslash"))!! + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after")) + } + + // PY-8788 + fun testRenameShadowingBuiltins() { + val fileName = "RenameShadowingBuiltins.py" + myFixture.configureByFile(fileName) + myFixture.enableInspections(PyShadowingBuiltinsInspection::class.java) + myFixture.checkHighlighting(true, false, true) + val intentionAction = myFixture.getAvailableIntention(PyBundle.message("QFIX.NAME.rename.element"))!! + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkResultByFile(graftBeforeExt(fileName, "_after")) + } + + // PY-8788 + fun testRenameFunctionShadowingBuiltins() { + val fileName = "RenameFunctionShadowingBuiltins.py" + myFixture.configureByFile(fileName) + myFixture.enableInspections(PyShadowingBuiltinsInspection::class.java) + myFixture.checkHighlighting(true, false, true) + val intentionAction = myFixture.getAvailableIntention(PyBundle.message("QFIX.NAME.rename.element"))!! + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkResultByFile(graftBeforeExt(fileName, "_after")) + } + + fun testIgnoreShadowingBuiltins() { + myFixture.configureByFile("IgnoreShadowingBuiltins.py") + myFixture.enableInspections(PyShadowingBuiltinsInspection::class.java) + val intentionAction = myFixture.getAvailableIntention("Ignore shadowed built-in name \"open\"")!! + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkHighlighting(true, false, true) + } + + fun testImplementAbstractProperty() { + doInspectionTest( + "ImplementAbstractProperty.py", + PyAbstractClassInspection::class.java, + PyBundle.message("QFIX.NAME.implement.methods"), + true, + true + ) + } + + fun testImplementAbstractProperty1() { + doInspectionTest( + "ImplementAbstractProperty.py", + PyAbstractClassInspection::class.java, + PyBundle.message("QFIX.NAME.implement.methods"), + true, + true + ) + } + + fun testImplementAbstractOrder() { + doInspectionTest( + "ImplementAbstractOrder.py", + PyAbstractClassInspection::class.java, + PyBundle.message("QFIX.NAME.implement.methods"), + true, + true + ) + } + + fun testRemovingUnderscoresInNumericLiterals() { + myFixture.configureByText(PythonFileType.INSTANCE, "1_0_0") + + val action = myFixture.findSingleIntention(PyPsiBundle.message("QFIX.NAME.remove.underscores.in.numeric")) + myFixture.launchAction(action) + + myFixture.checkResult("100") + } + + // PY-20452 + fun testRemoveRedundantEscapeInOnePartRegExp() { + myFixture.enableInspections(RegExpRedundantEscapeInspection()) + myFixture.configureByText(PythonFileType.INSTANCE, "import re\nre.compile(\"(?P((\\/(?P.+))?))\")\")") + + val quickFixes = myFixture.getAllQuickFixes() + assertEquals(1, quickFixes.size) + + val removeRedundantEscapeFix = quickFixes[0] + assertEquals("Remove redundant escape", removeRedundantEscapeFix.text) + + myFixture.launchAction(removeRedundantEscapeFix) + myFixture.checkResult("import re\nre.compile(\"(?P((/(?P.+))?))\")\")") + } + + // PY-20452 + fun testRemoveRedundantEscapeInMultiPartRegExp() { + myFixture.enableInspections(RegExpRedundantEscapeInspection()) + myFixture.configureByText( + PythonFileType.INSTANCE, """ + import re + re.compile("(?P" + "((\/(?P.+))?))") + """.trimIndent() + ) + + val quickFixes = myFixture.getAllQuickFixes() + assertEquals(1, quickFixes.size) + + val removeRedundantEscapeFix = quickFixes[0] + assertEquals("Remove redundant escape", removeRedundantEscapeFix.text) + + myFixture.launchAction(removeRedundantEscapeFix) + myFixture.checkResult( + """ + import re + re.compile("(?P" + "((/(?P.+))?))") + """.trimIndent() + ) + } + + // PY-8174 + fun testChangeSignatureKeywordAndPositionalParameters() { + doInspectionTest( + PyArgumentListInspection::class.java, + "Change the signature of f(x, foo)", + true, + true + ) + } + + // PY-8174 + fun testChangeSignatureAddKeywordOnlyParameter() { + runWithLanguageLevel(LanguageLevel.PYTHON34) { + doInspectionTest( + PyArgumentListInspection::class.java, + "Change the signature of func(x, *args, foo)", + true, + true + ) + } + } + + // PY-8174 + fun testChangeSignatureNewParametersNames() { + doInspectionTest( + PyArgumentListInspection::class.java, + "Change the signature of func(i1)", + true, + true + ) + } + + // PY-53671 + fun testChangeSignatureOfExportedBoundMethod() { + runWithLanguageLevel(LanguageLevel.getLatest()) { + doMultiFilesInspectionTest( + PyArgumentListInspection::class.java, + "Change the signature of method(self, a, b)", + "mod.py" + ) + } + } + + // PY-8174 + fun testChangeSignatureParametersDefaultValues() { + doInspectionTest( + PyArgumentListInspection::class.java, + "Change the signature of func()", + true, + true + ) + } + + fun testAddKwargsToNewMethodIncompatibleWithInit() { + doInspectionTest( + PyInitNewSignatureInspection::class.java, + "Change the signature of __new__(cls)", + true, + true + ) + } + + fun testAddKwargsToIncompatibleOverridingMethod() { + doInspectionTest( + PyMethodOverridingInspection::class.java, + "Change the signature of m(self)", + true, + true + ) + } + + // PY-30789 + fun testSetImportedABCMetaAsMetaclassPy2() { + doInspectionTest( + "PyAbstractClassInspection/quickFix/SetImportedABCMetaAsMetaclassPy2/main.py", + PyAbstractClassInspection::class.java, + "Set '${PyNames.ABC_META}' as metaclass", + true, + true + ) + } + + @NonNls + override fun getTestDataPath(): String = PythonTestUtil.getTestDataPath() + "/inspections/" + + private fun doInspectionTest( + inspectionClass: Class, + quickFixName: String, + applyFix: Boolean, + available: Boolean, + ) { + doInspectionTest(getTestName(false) + ".py", inspectionClass, quickFixName, applyFix, available) + } + + protected fun doInspectionTest( + @TestDataFile @NonNls testFileName: String, + inspectionClass: Class, + @NonNls quickFixName: String, + applyFix: Boolean, + available: Boolean, + ) { + doInspectionTest(arrayOf(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 fun doInspectionTest( + @NonNls testFiles: Array, + inspectionClass: Class, + @NonNls quickFixName: String, + applyFix: Boolean, + available: Boolean, + ) { + myFixture.enableInspections(inspectionClass) + myFixture.configureByFiles(*testFiles) + myFixture.checkHighlighting(true, false, false) + val intentionActions = myFixture.filterAvailableIntentions(quickFixName) + if (available) { + if (intentionActions.isEmpty()) { + val intentionNames = myFixture.availableIntentions.map({ it.text }) + throw AssertionError( + "Quickfix starting with \"$quickFixName\" is not available. " + + "Available intentions:\n${StringUtil.join(intentionNames, "\n")}" + ) + } + if (intentionActions.size > 1) { + throw AssertionError("There are more than one quickfix with the name \"$quickFixName\"") + } + if (applyFix) { + myFixture.launchAction(intentionActions[0]) + NonBlockingReadActionImpl.waitForAsyncTaskCompletion() + myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after"), true) + } + } + else { + assertEmpty("Quick fix \"$quickFixName\" should not be available", intentionActions) + } + } + + private fun doMultiFilesInspectionTest( + inspectionClass: Class, + intentionStr: String, + modifiedFile: String, + ) { + myFixture.enableInspections(inspectionClass) + myFixture.copyDirectoryToProject(getTestName(true), "") + myFixture.configureFromTempProjectFile(getTestName(true) + ".py") + myFixture.checkHighlighting(true, false, false) + val intentionAction = myFixture.findSingleIntention(intentionStr) + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + val expectedFile = getTestName(true) + "/" + graftBeforeExt(modifiedFile, "_after") + myFixture.checkResultByFile(modifiedFile, expectedFile, true) + } + + companion object { + // Turns "name.ext" to "name_insertion.ext" + @NonNls + private fun graftBeforeExt(name: String, insertion: String): String { + var dotpos = name.indexOf('.') + if (dotpos < 0) dotpos = name.length + return name.substring(0, dotpos) + insertion + name.substring(dotpos) + } + } +} diff --git a/python/testSrc/com/jetbrains/python/packaging/management/TestPypiPackageCache.kt b/python/testSrc/com/jetbrains/python/packaging/management/TestPypiPackageCache.kt new file mode 100644 index 000000000000..cc9b1a8c0180 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/packaging/management/TestPypiPackageCache.kt @@ -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 = emptySet() + override val packages: Set + get() { + return testPackages + } + + override suspend fun reloadCache(force: Boolean): Result { + return Result.success(Unit) + } +} \ No newline at end of file